Compare commits

...

58 Commits

Author SHA1 Message Date
bbd0b48f0b Merge pull request 'Update module modernc.org/sqlite to v1.30.2' (#38) from renovate/modernc.org-sqlite-1.x into main
Reviewed-on: #38
2024-07-14 19:35:32 +00:00
d91bdea878 Merge pull request 'Update module github.com/charmbracelet/lipgloss to v0.12.1' (#37) from renovate/github.com-charmbracelet-lipgloss-0.x into main
Reviewed-on: #37
2024-07-14 19:35:23 +00:00
Renovate Bot
b5555f778d Update module github.com/charmbracelet/lipgloss to v0.12.1 2024-07-12 09:47:57 -07:00
Renovate Bot
2a7ceb342d Update module modernc.org/sqlite to v1.30.2 2024-07-12 08:47:56 -07:00
4563d03770 Merge pull request 'Update module github.com/charmbracelet/lipgloss to v0.11.1' (#36) from renovate/github.com-charmbracelet-lipgloss-0.x into main
Reviewed-on: #36
2024-07-10 19:50:20 +00:00
Renovate Bot
a8f2671a44 Update module github.com/charmbracelet/lipgloss to v0.11.1 2024-07-10 07:48:04 -07:00
9f92321ca5 Merge pull request 'Update module google.golang.org/api to v0.188.0' (#35) from renovate/google.golang.org-api-0.x into main
Reviewed-on: #35
2024-07-10 00:42:53 +00:00
Renovate Bot
e8ff2971ca Update module google.golang.org/api to v0.188.0 2024-07-09 10:48:06 -07:00
4a9c315526 Merge pull request 'Update module golang.org/x/net to v0.27.0' (#29) from renovate/golang.org-x-net-0.x into main
Reviewed-on: #29
2024-07-08 02:09:50 +00:00
41792410e7 Merge pull request 'Update module github.com/charmbracelet/bubbletea to v0.26.6' (#26) from renovate/github.com-charmbracelet-bubbletea-0.x into main
Reviewed-on: #26
2024-07-08 02:09:45 +00:00
Renovate Bot
3044be396c Update module golang.org/x/net to v0.27.0 2024-07-07 09:23:51 -07:00
Renovate Bot
4b5237f381 Update module github.com/charmbracelet/bubbletea to v0.26.6 2024-07-07 09:23:46 -07:00
302f60ac4f Merge pull request 'Update module github.com/charmbracelet/lipgloss to v0.11.0' (#27) from renovate/github.com-charmbracelet-lipgloss-0.x into main
Reviewed-on: #27
2024-07-07 16:13:08 +00:00
d7de010442 Merge pull request 'Update module google.golang.org/api to v0.187.0' (#32) from renovate/google.golang.org-api-0.x into main
Reviewed-on: #32
2024-07-07 16:12:55 +00:00
Renovate Bot
5a90fb228d Update module google.golang.org/api to v0.187.0 2024-07-06 14:22:02 -07:00
Renovate Bot
18e355d36c Update module github.com/charmbracelet/lipgloss to v0.11.0 2024-07-06 14:21:47 -07:00
f740e96003
bye frand 2024-07-06 14:08:49 -07:00
6a6a1ada21 Merge pull request 'Update module github.com/charmbracelet/bubbles to v0.18.0' (#25) from renovate/github.com-charmbracelet-bubbles-0.x into main
Reviewed-on: #25
2024-07-06 21:07:20 +00:00
c2352547c7 Merge pull request 'Update module github.com/spf13/cobra to v1.8.1' (#28) from renovate/github.com-spf13-cobra-1.x into main
Reviewed-on: #28
2024-07-06 21:07:12 +00:00
ae9d766246 Merge pull request 'Update module modernc.org/sqlite to v1.30.1' (#33) from renovate/modernc.org-sqlite-1.x into main
Reviewed-on: #33
2024-07-06 21:06:53 +00:00
Renovate Bot
eb39b4fb05 Update module modernc.org/sqlite to v1.30.1 2024-07-06 13:47:47 -07:00
Renovate Bot
0d974324ca Update module github.com/spf13/cobra to v1.8.1 2024-07-06 13:47:22 -07:00
Renovate Bot
9ea787f1ae Update module github.com/charmbracelet/bubbles to v0.18.0 2024-07-06 13:47:05 -07:00
d8fb4da2a7 Update renovate.json 2024-07-06 20:35:50 +00:00
c2a4655cdd Merge pull request 'Update golang Docker tag to v1.22' (#23) from renovate/golang-1.x into main
Reviewed-on: #23
2024-07-06 20:26:19 +00:00
d98a7e82dc Merge pull request 'Update module github.com/zmb3/spotify/v2 to v2.4.2' (#22) from renovate/github.com-zmb3-spotify-v2-2.x into main
Reviewed-on: #22
2024-07-06 20:26:04 +00:00
Renovate Bot
7092c3d520 Update golang Docker tag to v1.22 2024-07-06 13:18:09 -07:00
Renovate Bot
e7e9eedb3b Update module github.com/zmb3/spotify/v2 to v2.4.2 2024-07-06 13:18:07 -07:00
ad529e7aab Merge pull request 'Configure Renovate' (#21) from renovate/configure into main
Reviewed-on: #21
2024-07-06 20:11:05 +00:00
Renovate Bot
43300710ef Add renovate.json 2024-07-06 13:08:20 -07:00
6bbcd97b1e Merge pull request 'this should be println' (#20) from println into main
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #20
2023-11-06 07:02:48 +00:00
a
2925b2c432
this should be print
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2023-11-05 20:42:20 -06:00
4cd781d5db
pipeline -> steps
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-20 10:55:20 -07:00
d4f4b142f7
update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-20 10:53:40 -07:00
2a942a3803
update, checks in format, rename alias for dl
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-10-20 10:52:55 -07:00
95c64ebd70
add force argument to now playing
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-14 09:47:03 -07:00
e39355fb69
download_cover
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-14 09:27:58 -07:00
696ee7e263
remove print
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
2023-09-26 15:41:02 -07:00
84b3bd2f90
add case for playlist itme
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
2023-09-26 15:33:11 -07:00
b569bd5bd6
uncomment 2023-07-25 21:35:38 -07:00
e05573c2cf
remove file 2023-07-25 21:32:07 -07:00
232a1cd3b6
error handling, fix initial auth not erroring, remove depricated functions, update libraries 2023-07-25 21:31:43 -07:00
a34633ec6d
fix pagination
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-04-23 15:10:35 -07:00
5081c44304
oopsie
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-04-16 11:31:54 -07:00
7e22c90311
better messages/feedback
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-16 11:30:37 -07:00
efb03c1aad
fix skipping within queue while playing in a context
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-16 10:58:50 -07:00
b5f76fb321
queue tracks, view queue, queue updates when open, hotkey clean up
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-16 10:47:26 -07:00
6ed3453dae
fix crash if no queue / not playing anything
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-16 00:12:50 -07:00
a386f90ec5
queue wow
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-16 00:05:23 -07:00
81d5135d1a
sanitize playlist descriptions
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-15 23:21:48 -07:00
693b228194
make album info less stupid looking
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-15 23:04:18 -07:00
c124ec4eb9
fix loadmoreitems being called a billion times
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-04-14 19:45:59 -07:00
ec60177128
update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-14 19:29:05 -07:00
8f179f0bc6
add experimental youtube-link command. fix pagination issues throughout tui 2023-04-14 19:28:31 -07:00
ee9c2299dc
better toggle
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-04-07 09:25:10 -07:00
2fbeb6d21a
make code less bad
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-06 18:11:06 -07:00
55f9c73966
fix paging for playlists
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-05 19:40:24 -07:00
50d5cd18a8
version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-03-11 21:39:46 -08:00
46 changed files with 1107 additions and 475 deletions

87
.golangci.yml Normal file
View File

@ -0,0 +1,87 @@
run:
deadline: 10m
skip-dirs:
- hack
linters:
disable-all: true
enable:
- gofmt
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- gocritic
- bodyclose
- gosec
- prealloc
- unconvert
- unused
linters-settings:
gocritic:
# Which checks should be enabled; can't be combined with 'disabled-checks';
# See https://go-critic.github.io/overview#checks-overview
# To check which checks are enabled run `GL_DEBUG=gocritic ./build/bin/golangci-lint run`
# By default list of stable checks is used.
enabled-checks:
- ruleguard
- truncateCmp
# Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty
disabled-checks:
- captLocal
- assignOp
- 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".
enabled-tags:
- performance
- diagnostic
- opinionated
disabled-tags:
- experimental
settings:
hugeParam:
# size in bytes that makes the warning trigger (default 80)
sizeThreshold: 1000
rangeExprCopy:
# size in bytes that makes the warning trigger (default 512)
sizeThreshold: 512
# whether to check test functions (default true)
skipTestFuncs: true
truncateCmp:
# whether to skip int/uint/uintptr types (default true)
skipArchDependent: true
underef:
# whether to skip (*x).method() calls where x is a pointer receiver (default true)
skipRecvDeref: true
govet:
disable:
- deepequalerrors
- fieldalignment
- shadow
- unsafeptr
goconst:
min-len: 2
min-occurrences: 2
gofmt:
auto-fix: false
issues:
exclude-rules:
- linters:
- golint
text: "should be"
- linters:
- errcheck
text: "not checked"
- linters:
- staticcheck
text: "SA(1019|1029|5011)"

View File

@ -17,6 +17,8 @@ builds:
ignore:
- goos: windows
goarch: "386"
ldflags:
- -s -w -X git.asdf.cafe/abs3nt/gospt/src.cmd.Version={{.Version}}
archives:
- format: tar.gz

View File

@ -1,6 +1,6 @@
pipeline:
steps:
build:
image: golang:1.19
image: golang:1.22
commands:
- go mod tidy
- go build -o gospt

20
cmd/download_cover.go Normal file
View File

@ -0,0 +1,20 @@
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)
},
}

View File

@ -19,7 +19,7 @@ var linkCmd = &cobra.Command{
fmt.Println(err)
os.Exit(1)
}
fmt.Print(link)
fmt.Println(link)
},
}

View File

@ -25,6 +25,6 @@ var nextCmd = &cobra.Command{
return err
}
}
return commands.Next(ctx, skipAmt)
return commands.Next(ctx, skipAmt, false)
},
}

View File

@ -13,7 +13,8 @@ var nowPlayingCmd = &cobra.Command{
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)
commands.NowPlaying(ctx, args)
},
}

View File

@ -21,11 +21,10 @@ import (
var (
// Used for flags.
ctx *gctx.Context
commands *cmds.Commands
cfgFile string
userLicense string
verbose bool
ctx *gctx.Context
commands *cmds.Commands
cfgFile string
verbose bool
rootCmd = &cobra.Command{
Use: "gospt",
@ -90,7 +89,12 @@ func initConfig() {
}
if config.Values.ClientSecretCmd != "" {
args := strings.Fields(config.Values.ClientSecretCmd)
secret, err := exec.Command(args[0], args[1:]...).Output()
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)
}

23
cmd/version.go Normal file
View File

@ -0,0 +1,23 @@
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)
}

28
cmd/youtube-link.go Normal file
View File

@ -0,0 +1,28 @@
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)
}

108
go.mod
View File

@ -1,63 +1,83 @@
module git.asdf.cafe/abs3nt/gospt
go 1.19
go 1.21
require (
gfx.cafe/util/go/frand v0.0.0-20230121041905-80dafb1e973e
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.14.0
github.com/charmbracelet/bubbletea v0.23.1
github.com/charmbracelet/lipgloss v0.6.0
github.com/cristalhq/aconfig v0.18.3
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.6.1
github.com/zmb3/spotify/v2 v2.3.1
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5
modernc.org/sqlite v1.20.4
tuxpa.in/a/zlog v1.60.0
github.com/spf13/cobra v1.8.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
)
require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
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
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.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/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/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/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/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // 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.16 // 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.14 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/mattn/go-runewidth v0.0.15 // 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.13.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rs/zerolog v1.28.0 // indirect
github.com/sahilm/fuzzy v0.1.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
golang.org/x/mod v0.3.0 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.52.1 // 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
)

361
go.sum
View File

@ -13,12 +13,29 @@ 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/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/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=
@ -31,58 +48,86 @@ 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=
gfx.cafe/util/go/frand v0.0.0-20230121041905-80dafb1e973e h1:A62zlsu3HkEAVRIb+cCpRIpSTmd047+ABV1KC2RsI2U=
gfx.cafe/util/go/frand v0.0.0-20230121041905-80dafb1e973e/go.mod h1:LNHxMJl0WnIr5+OChYxlVopxk+j7qxZv0XvWCzB6uGE=
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/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
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.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og=
github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
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/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
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/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.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
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.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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.3 h1:Or12LIWIF+2mQpcGWA2PQnNc55+WiHFAqRjYh/pQNtM=
github.com/cristalhq/aconfig v0.18.3/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/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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
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/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/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/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=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -103,9 +148,13 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
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 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
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=
@ -115,8 +164,10 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -126,22 +177,33 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
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/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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/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/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.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
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=
@ -156,68 +218,95 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb
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 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
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.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
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/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.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
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.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
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-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
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.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/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
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.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
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/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=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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/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=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zmb3/spotify/v2 v2.3.1 h1:aEyIPotROM3JJjHMCImFROgnPIUpzVo8wymYSaPSd9w=
github.com/zmb3/spotify/v2 v2.3.1/go.mod h1:+LVh9CafHu7SedyqYmEf12Rd01dIVlEL845yNhksW0E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zmb3/spotify/v2 v2.4.2 h1:j3yNN5lKVEMZQItJF4MHCSZbfNWmXO+KaC+3RFaLlLc=
github.com/zmb3/spotify/v2 v2.4.2/go.mod h1:XOV7BrThayFYB9AAfB+L0Q0wyxBuLCARk4fI/ZXCBW8=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
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=
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=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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/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=
@ -247,8 +336,11 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
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/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=
@ -275,16 +367,28 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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/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 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o=
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/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=
@ -293,7 +397,12 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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/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=
@ -321,28 +430,51 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/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 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
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/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
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/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=
@ -386,12 +518,13 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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/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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -409,14 +542,21 @@ 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/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=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
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 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
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=
@ -446,6 +586,16 @@ 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/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=
@ -458,6 +608,13 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
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/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=
@ -470,8 +627,10 @@ 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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/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=
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=
@ -487,32 +646,36 @@ 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=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
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/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/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=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
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/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=
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.60.0 h1:bU4wJk6nwvaFsKIvKKxgGM0uO+Z2kaE8LzgRiQ4NCRw=
tuxpa.in/a/zlog v1.60.0/go.mod h1:1t8SX1a4zLy+p6ylGn6m1ZXnssTPr/2ErdPjjSP+C2k=
tuxpa.in/a/zlog v1.61.0 h1:7wrS6G4QwpnOmgHRQknrr7IgiMXrfGpekkU0PjM9FhE=
tuxpa.in/a/zlog v1.61.0/go.mod h1:CNpMe8laDHLSypx/DyxfX1S0oyxUydeo3aGTEbtRBhg=

View File

@ -1,7 +1,7 @@
package main
import (
"git.asdf.cafe/abs3nt/gospt/src/cmd"
"git.asdf.cafe/abs3nt/gospt/cmd"
)
func main() {

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["config:recommended"]
}

View File

@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"path/filepath"
"time"
"tuxpa.in/a/zlog/log"
@ -91,7 +92,7 @@ func GetClient(ctx *gctx.Context) (*spotify.Client, error) {
if err != nil {
return nil, err
}
err = os.WriteFile(authFilePath, out, 0o644)
err = os.WriteFile(authFilePath, out, 0o600)
if err != nil {
return nil, fmt.Errorf("failed to save auth")
}
@ -102,11 +103,12 @@ func GetClient(ctx *gctx.Context) (*spotify.Client, error) {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("Got request for:", r.URL.String())
})
server := &http.Server{
Addr: fmt.Sprintf(":%s", config.Values.Port),
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
err := http.ListenAndServe(fmt.Sprintf(":%s", config.Values.Port), nil)
if err != nil {
panic(err)
}
server.ListenAndServe()
}()
url := auth.AuthURL(state)
fmt.Println(url)
@ -115,6 +117,7 @@ func GetClient(ctx *gctx.Context) (*spotify.Client, error) {
// wait for auth to complete
client := <-ch
server.Shutdown(ctx)
// use the client to make calls that require authorization
user, err := client.CurrentUser(ctx)
if err != nil {
@ -137,7 +140,7 @@ func completeAuth(w http.ResponseWriter, r *http.Request) {
if err != nil {
panic(err.Error())
}
err = os.WriteFile(filepath.Join(configDir, "gospt/auth.json"), out, 0o644)
err = os.WriteFile(filepath.Join(configDir, "gospt/auth.json"), out, 0o600)
if err != nil {
panic("FAILED TO SAVE AUTH")
}

2
src/cache/cache.go vendored
View File

@ -42,7 +42,7 @@ func (c *Cache) save(m map[string]CacheEntry) error {
return err
}
log.Trace().Str("tosave", string(payload)).Msg("saving cache")
err = os.WriteFile(c.Root, payload, 0640)
err = os.WriteFile(c.Root, payload, 0o600)
if err != nil {
return err
}

View File

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"math"
"math/rand"
"net/url"
"os"
"path/filepath"
@ -14,13 +15,13 @@ import (
"sync"
"time"
"gfx.cafe/util/go/frand"
"github.com/zmb3/spotify/v2"
_ "modernc.org/sqlite"
"git.asdf.cafe/abs3nt/gospt/src/auth"
"git.asdf.cafe/abs3nt/gospt/src/cache"
"git.asdf.cafe/abs3nt/gospt/src/gctx"
"github.com/zmb3/spotify/v2"
_ "modernc.org/sqlite"
"git.asdf.cafe/abs3nt/gospt/src/youtube"
)
type Commands struct {
@ -147,7 +148,8 @@ func (c *Commands) UserArtists(ctx *gctx.Context, page int) (*spotify.FullArtist
}
func (c *Commands) ArtistAlbums(ctx *gctx.Context, artist spotify.ID, page int) (*spotify.SimpleAlbumPage, error) {
albums, err := c.Client().GetArtistAlbums(ctx, artist, []spotify.AlbumType{1, 2, 3, 4}, spotify.Market(spotify.CountryUSA), spotify.Limit(50), spotify.Offset((page-1)*50))
albums, err := c.Client().
GetArtistAlbums(ctx, 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
}
@ -155,7 +157,8 @@ func (c *Commands) ArtistAlbums(ctx *gctx.Context, artist spotify.ID, page int)
}
func (c *Commands) Search(ctx *gctx.Context, search string, page int) (*spotify.SearchResult, error) {
result, err := c.Client().Search(ctx, search, spotify.SearchTypeAlbum|spotify.SearchTypeArtist|spotify.SearchTypeTrack|spotify.SearchTypePlaylist, spotify.Limit(50), spotify.Offset((page-1)*50))
result, err := c.Client().
Search(ctx, search, spotify.SearchTypeAlbum|spotify.SearchTypeArtist|spotify.SearchTypeTrack|spotify.SearchTypePlaylist, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return nil, err
}
@ -163,7 +166,8 @@ func (c *Commands) Search(ctx *gctx.Context, search string, page int) (*spotify.
}
func (c *Commands) AlbumTracks(ctx *gctx.Context, album spotify.ID, page int) (*spotify.SimpleTrackPage, error) {
tracks, err := c.Client().GetAlbumTracks(ctx, album, spotify.Limit(50), spotify.Offset((page-1)*50), spotify.Market(spotify.CountryUSA))
tracks, err := c.Client().
GetAlbumTracks(ctx, album, spotify.Limit(50), spotify.Offset((page-1)*50), spotify.Market(spotify.CountryUSA))
if err != nil {
return nil, err
}
@ -171,11 +175,11 @@ func (c *Commands) AlbumTracks(ctx *gctx.Context, album spotify.ID, page int) (*
}
func (c *Commands) UserAlbums(ctx *gctx.Context, page int) (*spotify.SavedAlbumPage, error) {
albums, err := c.Client().CurrentUsersAlbums(ctx, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return nil, err
}
return albums, nil
return c.Client().CurrentUsersAlbums(ctx, spotify.Limit(50), spotify.Offset((page-1)*50))
}
func (c *Commands) UserQueue(ctx *gctx.Context) (*spotify.Queue, error) {
return c.Client().GetQueue(ctx)
}
func (c *Commands) PlayUrl(ctx *gctx.Context, args []string) error {
@ -237,7 +241,7 @@ func (c *Commands) QueueSong(ctx *gctx.Context, id spotify.ID) error {
return nil
}
func (c *Commands) PlaySongInPlaylist(ctx *gctx.Context, context *spotify.URI, offset int) error {
func (c *Commands) PlaySongInPlaylist(ctx *gctx.Context, context *spotify.URI, offset *int) error {
e := c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
PlaybackContext: context,
@ -253,7 +257,6 @@ func (c *Commands) PlaySongInPlaylist(ctx *gctx.Context, context *spotify.URI, o
PlaybackContext: context,
DeviceID: &deviceID,
})
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice(ctx)
@ -304,9 +307,6 @@ func (c *Commands) PlayLikedSongs(ctx *gctx.Context, position int) error {
}
err = c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &playlist.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: 0,
},
})
if err != nil {
if isNoActiveError(err) {
@ -316,10 +316,7 @@ func (c *Commands) PlayLikedSongs(ctx *gctx.Context, position int) error {
}
err = c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &playlist.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: 0,
},
DeviceID: &deviceID,
DeviceID: &deviceID,
})
if err != nil {
return err
@ -381,16 +378,13 @@ func (c *Commands) RadioGivenArtist(ctx *gctx.Context, artist spotify.SimpleArti
}
c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: 0,
},
})
err = c.Client().Repeat(ctx, "context")
if err != nil {
return err
}
for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1
id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]},
}
@ -469,10 +463,7 @@ func (c *Commands) RadioGivenSong(ctx *gctx.Context, song spotify.SimpleTrack, p
}
err = c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: 0,
},
PositionMs: pos,
PositionMs: pos,
})
if err != nil {
if isNoActiveError(err) {
@ -482,11 +473,8 @@ func (c *Commands) RadioGivenSong(ctx *gctx.Context, song spotify.SimpleTrack, p
}
err = c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: 0,
},
DeviceID: &deviceID,
PositionMs: pos,
DeviceID: &deviceID,
PositionMs: pos,
})
if err != nil {
return err
@ -498,7 +486,7 @@ func (c *Commands) RadioGivenSong(ctx *gctx.Context, song spotify.SimpleTrack, p
return err
}
for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1
id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]},
}
@ -528,10 +516,18 @@ func (c *Commands) RadioGivenSong(ctx *gctx.Context, song spotify.SimpleTrack, p
return nil
}
func (c *Commands) DeleteTracksFromPlaylist(ctx *gctx.Context, tracks []spotify.ID, playlist spotify.ID) error {
_, err := c.Client().RemoveTracksFromPlaylist(ctx, playlist, tracks...)
if err != nil {
return err
}
return nil
}
func (c *Commands) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
song_id := string(song)
sqlStmt := `SELECT id FROM radio WHERE id = ?`
err := db.QueryRow(sqlStmt, string(song_id)).Scan(&song_id)
err := db.QueryRow(sqlStmt, song_id).Scan(&song_id)
if err != nil {
if err != sql.ErrNoRows {
return false, err
@ -562,16 +558,13 @@ func (c *Commands) Radio(ctx *gctx.Context) error {
if err != nil {
return err
}
seed_song = tracks.Tracks[frand.Intn(len(tracks.Tracks))].SimpleTrack
} else {
if !current_song.Playing {
tracks, err := c.Client().CurrentUsersTracks(ctx, spotify.Limit(10))
if err != nil {
return err
}
seed_song = tracks.Tracks[frand.Intn(len(tracks.Tracks))].SimpleTrack
seed_song = tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack
} else if !current_song.Playing {
tracks, err := c.Client().CurrentUsersTracks(ctx, spotify.Limit(10))
if err != nil {
return err
}
seed_song = tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack
}
return c.RadioGivenSong(ctx, seed_song, current_song.Progress)
}
@ -587,6 +580,7 @@ func (c *Commands) RefillRadio(ctx *gctx.Context) error {
to_remove := []spotify.ID{}
radioPlaylist, db, err := c.GetRadioPlaylist(ctx, "")
if err != nil {
return err
}
if status.PlaybackContext.URI != radioPlaylist.URI {
@ -598,20 +592,17 @@ func (c *Commands) RefillRadio(ctx *gctx.Context) error {
return fmt.Errorf("orig playlist items: %w", err)
}
found := false
page := 0
for !found {
for {
tracks, err := c.Client().GetPlaylistItems(ctx, radioPlaylist.ID, spotify.Limit(50), spotify.Offset(page*50))
if err != nil {
return fmt.Errorf("tracks: %w", err)
}
if len(tracks.Items) == 0 {
found = true
break
}
for _, track := range tracks.Items {
if track.Track.Track.ID == status.Item.ID {
found = true
break
}
to_remove = append(to_remove, track.Track.Track.ID)
@ -630,7 +621,7 @@ func (c *Commands) RefillRadio(ctx *gctx.Context) error {
return fmt.Errorf("error clearing playlist: %w", err)
}
}
_, err = c.Client().RemoveTracksFromPlaylist(ctx, radioPlaylist.ID, trackGroups...)
c.Client().RemoveTracksFromPlaylist(ctx, radioPlaylist.ID, trackGroups...)
}
to_add := 500 - (playlistItems.Total - len(to_remove))
@ -642,14 +633,15 @@ func (c *Commands) RefillRadio(ctx *gctx.Context) error {
pages := int(math.Ceil(float64(total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(int(pages-1)) + 1
randomPage = rand.Intn(pages-1) + 1
}
playlistPage, err := c.Client().GetPlaylistItems(ctx, radioPlaylist.ID, spotify.Limit(50), spotify.Offset((randomPage-1)*50))
playlistPage, err := c.Client().
GetPlaylistItems(ctx, radioPlaylist.ID, spotify.Limit(50), spotify.Offset((randomPage-1)*50))
if err != nil {
return fmt.Errorf("playlist page: %w", err)
}
pageSongs := playlistPage.Items
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
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)
@ -684,6 +676,9 @@ func (c *Commands) RefillRadio(ctx *gctx.Context) error {
break
}
_, err = db.QueryContext(ctx, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", rec.String()))
if err != nil {
return err
}
queue = append(queue, rec)
}
to_add -= len(queue)
@ -696,7 +691,7 @@ func (c *Commands) RefillRadio(ctx *gctx.Context) error {
return fmt.Errorf("repeat: %w", err)
}
for to_add > 0 {
id := frand.Intn(len(recomendationIds)-2) + 1
id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]},
}
@ -762,7 +757,7 @@ func (c *Commands) Pause(ctx *gctx.Context) error {
func (c *Commands) TogglePlay(ctx *gctx.Context) error {
current, err := c.Client().PlayerCurrentlyPlaying(ctx)
if err != nil {
return err
return c.Play(ctx)
}
if !current.Playing {
return c.Play(ctx)
@ -794,7 +789,13 @@ func (c *Commands) Unlike(ctx *gctx.Context) error {
return nil
}
func (c *Commands) Next(ctx *gctx.Context, amt int) error {
func (c *Commands) Next(ctx *gctx.Context, amt int, inqueue bool) error {
if inqueue {
for i := 0; i < amt; i++ {
c.Client().Next(ctx)
}
return nil
}
if amt == 1 {
err := c.Client().Next(ctx)
if err != nil {
@ -825,9 +826,10 @@ func (c *Commands) Next(ctx *gctx.Context, amt int) error {
case "playlist":
found := false
currentTrackIndex := 0
page := 1
for !found {
page := 1
playlist, err := c.Client().GetPlaylistItems(ctx, spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]), spotify.Limit(50), spotify.Offset((page-1)*50))
playlist, err := c.Client().
GetPlaylistItems(ctx, spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]), spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return err
}
@ -840,19 +842,21 @@ func (c *Commands) Next(ctx *gctx.Context, amt int) error {
}
page++
}
pos := currentTrackIndex + amt
c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &current.PlaybackContext.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: currentTrackIndex + amt,
Position: &pos,
},
})
return nil
case "album":
found := false
currentTrackIndex := 0
page := 1
for !found {
page := 1
playlist, err := c.Client().GetAlbumTracks(ctx, spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]), spotify.Limit(50), spotify.Offset((page-1)*50))
playlist, err := c.Client().
GetAlbumTracks(ctx, spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]), spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return err
}
@ -865,10 +869,11 @@ func (c *Commands) Next(ctx *gctx.Context, amt int) error {
}
page++
}
pos := currentTrackIndex + amt
c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &current.PlaybackContext.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: currentTrackIndex + amt,
Position: &pos,
},
})
return nil
@ -903,10 +908,24 @@ func (c *Commands) Status(ctx *gctx.Context) error {
if err != nil {
return err
}
fmt.Println(state)
return nil
}
func (c *Commands) DownloadCover(ctx *gctx.Context, args []string) error {
destinationPath := filepath.Clean(args[0])
state, err := c.Client().PlayerState(ctx)
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
}
fmt.Println(state)
return nil
}
@ -918,15 +937,35 @@ func (c *Commands) Link(ctx *gctx.Context) (string, error) {
return state.Item.ExternalURLs["spotify"], nil
}
func (c *Commands) YoutubeLink(ctx *gctx.Context) (string, error) {
state, err := c.Client().PlayerState(ctx)
if err != nil {
return "", err
}
link := youtube.Search(state.Item.Artists[0].Name + state.Item.Name)
return link, nil
}
func (c *Commands) LinkContext(ctx *gctx.Context) (string, error) {
state, err := c.Client().PlayerState(ctx)
if err != nil {
return "", err
}
return string(state.PlaybackContext.ExternalURLs["spotify"]), nil
return state.PlaybackContext.ExternalURLs["spotify"], nil
}
func (c *Commands) NowPlaying(ctx *gctx.Context) error {
func (c *Commands) NowPlaying(ctx *gctx.Context, args []string) error {
if len(args) > 0 {
if args[0] == "force" {
current, err := c.Client().PlayerCurrentlyPlaying(ctx)
if err != nil {
return err
}
str := FormatSong(current)
fmt.Println(str)
return nil
}
}
song, err := cache.DefaultCache().GetOrDo("now_playing", func() (string, error) {
current, err := c.Client().PlayerCurrentlyPlaying(ctx)
if err != nil {
@ -943,17 +982,25 @@ func (c *Commands) NowPlaying(ctx *gctx.Context) error {
}
func FormatSong(current *spotify.CurrentlyPlaying) string {
icon := "▶"
out := "▶"
if !current.Playing {
icon = "⏸"
out = "⏸"
}
return fmt.Sprintf("%s %s - %s", icon, current.Item.Name, current.Item.Artists[0].Name)
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
}
func (c *Commands) Shuffle(ctx *gctx.Context) error {
state, err := c.Client().PlayerState(ctx)
if err != nil {
return fmt.Errorf("Failed to get current playstate")
return fmt.Errorf("failed to get current playstate")
}
err = c.Client().Shuffle(ctx, !state.ShuffleState)
if err != nil {
@ -966,7 +1013,7 @@ func (c *Commands) Shuffle(ctx *gctx.Context) error {
func (c *Commands) Repeat(ctx *gctx.Context) error {
state, err := c.Client().PlayerState(ctx)
if err != nil {
return fmt.Errorf("Failed to get current playstate")
return fmt.Errorf("failed to get current playstate")
}
newState := "off"
if state.RepeatState == "off" {
@ -985,16 +1032,12 @@ func (c *Commands) TrackList(ctx *gctx.Context, page int) (*spotify.SavedTrackPa
return c.Client().CurrentUsersTracks(ctx, spotify.Limit(50), spotify.Offset((page-1)*50))
}
func (c *Commands) GetQueue(ctx *gctx.Context) (*spotify.Queue, error) {
return c.Client().GetQueue(ctx)
}
func (c *Commands) Playlists(ctx *gctx.Context, page int) (*spotify.SimplePlaylistPage, error) {
return c.Client().CurrentUsersPlaylists(ctx, spotify.Limit(50), spotify.Offset((page-1)*50))
}
func (c *Commands) PlaylistTracks(ctx *gctx.Context, playlist spotify.ID, page int) (*spotify.PlaylistTrackPage, error) {
return c.Client().GetPlaylistTracks(ctx, playlist, spotify.Limit(50), spotify.Offset((page-1)*50))
func (c *Commands) PlaylistTracks(ctx *gctx.Context, playlist spotify.ID, page int) (*spotify.PlaylistItemPage, error) {
return c.Client().GetPlaylistItems(ctx, playlist, spotify.Limit(50), spotify.Offset((page-1)*50))
}
func (c *Commands) FormatState(state *spotify.PlayerState) (string, error) {
@ -1012,7 +1055,7 @@ func (c *Commands) PrintPlaying(current *spotify.CurrentlyPlaying) error {
if !current.Playing {
icon = "⏸"
}
fmt.Println(fmt.Sprintf("%s %s - %s", icon, current.Item.Name, current.Item.Artists[0].Name))
fmt.Printf("%s %s - %s\n", icon, current.Item.Name, current.Item.Artists[0].Name)
return nil
}
@ -1031,7 +1074,7 @@ func (c *Commands) SetDevice(ctx *gctx.Context, device spotify.PlayerDevice) err
return err
}
configDir, _ := os.UserConfigDir()
err = os.WriteFile(filepath.Join(configDir, "gospt/device.json"), out, 0o644)
err = os.WriteFile(filepath.Join(configDir, "gospt/device.json"), out, 0o600)
if err != nil {
return err
}
@ -1049,19 +1092,19 @@ func isNoActiveError(err error) bool {
func (c *Commands) RadioFromPlaylist(ctx *gctx.Context, playlist spotify.SimplePlaylist) error {
total := playlist.Tracks.Total
if total == 0 {
return fmt.Errorf("This playlist is empty")
return fmt.Errorf("this playlist is empty")
}
pages := int(math.Ceil(float64(total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(int(pages-1)) + 1
randomPage = rand.Intn(pages-1) + 1
}
playlistPage, err := c.Client().GetPlaylistItems(ctx, playlist.ID, spotify.Limit(50), spotify.Offset((randomPage-1)*50))
if err != nil {
return err
}
pageSongs := playlistPage.Items
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
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)
@ -1083,19 +1126,19 @@ func (c *Commands) RadioFromAlbum(ctx *gctx.Context, album spotify.SimpleAlbum)
}
total := tracks.Total
if total == 0 {
return fmt.Errorf("This playlist is empty")
return fmt.Errorf("this playlist is empty")
}
pages := int(math.Ceil(float64(total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(int(pages-1)) + 1
randomPage = rand.Intn(pages-1) + 1
}
albumTrackPage, err := c.AlbumTracks(ctx, album.ID, randomPage)
if err != nil {
return err
}
pageSongs := albumTrackPage.Tracks
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
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)
@ -1116,19 +1159,19 @@ func (c *Commands) RadioFromSavedTracks(ctx *gctx.Context) error {
return err
}
if savedSongs.Total == 0 {
return fmt.Errorf("You have no saved songs")
return fmt.Errorf("you have no saved songs")
}
pages := int(math.Ceil(float64(savedSongs.Total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(int(pages-1)) + 1
randomPage = rand.Intn(pages-1) + 1
}
trackPage, err := c.Client().CurrentUsersTracks(ctx, spotify.Limit(50), spotify.Offset(randomPage*50))
if err != nil {
return err
}
pageSongs := trackPage.Tracks
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
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 {
@ -1181,9 +1224,6 @@ func (c *Commands) RadioGivenList(ctx *gctx.Context, song_ids []spotify.ID, name
}
err = c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: 0,
},
})
if err != nil {
if isNoActiveError(err) {
@ -1193,10 +1233,7 @@ func (c *Commands) RadioGivenList(ctx *gctx.Context, song_ids []spotify.ID, name
}
err = c.Client().PlayOpt(ctx, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
PlaybackOffset: &spotify.PlaybackOffset{
Position: 0,
},
DeviceID: &deviceId,
DeviceID: &deviceId,
})
if err != nil {
return err
@ -1204,7 +1241,7 @@ func (c *Commands) RadioGivenList(ctx *gctx.Context, song_ids []spotify.ID, name
}
}
for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1
id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]},
}
@ -1261,29 +1298,6 @@ func (c *Commands) activateDevice(ctx *gctx.Context) (spotify.ID, error) {
return device.ID, nil
}
func (c *Commands) getDefaultDevice(ctx *gctx.Context) (spotify.ID, error) {
configDir, _ := os.UserConfigDir()
if _, err := os.Stat(filepath.Join(configDir, "gospt/device.json")); err == nil {
deviceFile, err := os.Open(filepath.Join(configDir, "gospt/device.json"))
if err != nil {
return "", err
}
defer deviceFile.Close()
deviceValue, err := io.ReadAll(deviceFile)
if err != nil {
return "", err
}
var device *spotify.PlayerDevice
err = json.Unmarshal(deviceValue, &device)
if err != nil {
return "", err
}
return device.ID, nil
} else {
return "", err
}
}
func (c *Commands) GetRadioPlaylist(ctx *gctx.Context, name string) (*spotify.FullPlaylist, *sql.DB, error) {
configDir, _ := os.UserConfigDir()
playlistFile, err := os.ReadFile(filepath.Join(configDir, "gospt/radio.json"))
@ -1299,13 +1313,17 @@ func (c *Commands) GetRadioPlaylist(ctx *gctx.Context, name string) (*spotify.Fu
return nil, nil, err
}
db, err := sql.Open("sqlite", filepath.Join(configDir, "gospt/radio.db"))
if err != nil {
return nil, nil, err
}
return playlist, db, nil
}
func (c *Commands) CreateRadioPlaylist(ctx *gctx.Context, name string) (*spotify.FullPlaylist, *sql.DB, error) {
// private flag doesnt work
configDir, _ := os.UserConfigDir()
playlist, err := c.Client().CreatePlaylistForUser(ctx, c.User(), name+" - autoradio", "Automanaged radio playlist", false, false)
playlist, err := c.Client().
CreatePlaylistForUser(ctx, c.User(), name+" - autoradio", "Automanaged radio playlist", false, false)
if err != nil {
return nil, nil, err
}
@ -1313,11 +1331,14 @@ func (c *Commands) CreateRadioPlaylist(ctx *gctx.Context, name string) (*spotify
if err != nil {
return nil, nil, err
}
err = os.WriteFile(filepath.Join(configDir, "gospt/radio.json"), raw, 0o644)
err = os.WriteFile(filepath.Join(configDir, "gospt/radio.json"), raw, 0o600)
if err != nil {
return nil, nil, err
}
db, err := sql.Open("sqlite", filepath.Join(configDir, "gospt/radio.db"))
if err != nil {
return nil, nil, err
}
db.QueryContext(ctx, "DROP TABLE IF EXISTS radio")
db.QueryContext(ctx, "CREATE TABLE IF NOT EXISTS radio (id string PRIMARY KEY)")
return playlist, db, nil

View File

@ -1,15 +1,14 @@
package tui
import (
"github.com/zmb3/spotify/v2"
"git.asdf.cafe/abs3nt/gospt/src/commands"
"git.asdf.cafe/abs3nt/gospt/src/gctx"
"github.com/zmb3/spotify/v2"
)
func HandlePlayWithContext(ctx *gctx.Context, commands *commands.Commands, uri *spotify.URI, pos int) {
var err error
err = commands.PlaySongInPlaylist(ctx, uri, pos)
func HandlePlayWithContext(ctx *gctx.Context, commands *commands.Commands, uri *spotify.URI, pos *int) {
err := commands.PlaySongInPlaylist(ctx, uri, pos)
if err != nil {
return
}
@ -87,15 +86,35 @@ func HandlePlayTrack(ctx *gctx.Context, commands *commands.Commands, track spoti
if err != nil {
return
}
err = commands.Next(ctx, 1)
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) {
var err error
err = commands.SetDevice(ctx, player)
err := commands.SetDevice(ctx, player)
if err != nil {
return
}

View File

@ -8,10 +8,14 @@ import (
)
func (m *mainModel) LoadMoreItems() {
loading = true
defer func() {
page++
loading = false
}()
switch m.mode {
case "artist":
albums, err := m.commands.ArtistAlbums(m.ctx, m.artist.ID, (page + 1))
page++
if err != nil {
return
}
@ -31,7 +35,6 @@ func (m *mainModel) LoadMoreItems() {
return
case "artists":
artists, err := m.commands.UserArtists(m.ctx, (page + 1))
page++
if err != nil {
return
}
@ -51,7 +54,6 @@ func (m *mainModel) LoadMoreItems() {
return
case "album":
tracks, err := m.commands.AlbumTracks(m.ctx, m.album.ID, (page + 1))
page++
if err != nil {
return
}
@ -72,7 +74,6 @@ func (m *mainModel) LoadMoreItems() {
return
case "albums":
albums, err := m.commands.UserAlbums(m.ctx, (page + 1))
page++
if err != nil {
return
}
@ -92,7 +93,6 @@ func (m *mainModel) LoadMoreItems() {
return
case "main":
playlists, err := m.commands.Playlists(m.ctx, (page + 1))
page++
if err != nil {
return
}
@ -110,19 +110,18 @@ func (m *mainModel) LoadMoreItems() {
main_updates <- m
return
case "playlist":
tracks, err := m.commands.PlaylistTracks(m.ctx, m.playlist.ID, (page + 1))
page++
playlistItems, err := m.commands.PlaylistTracks(m.ctx, m.playlist.ID, (page + 1))
if err != nil {
return
}
items := []mainItem{}
for _, track := range tracks.Tracks {
for _, item := range playlistItems.Items {
items = append(items, mainItem{
Name: track.Track.Name,
Artist: track.Track.Artists[0],
Duration: track.Track.TimeDuration().Round(time.Second).String(),
ID: track.Track.ID,
Desc: track.Track.Artists[0].Name + " - " + 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(),
})
}
for _, item := range items {
@ -132,11 +131,9 @@ func (m *mainModel) LoadMoreItems() {
return
case "tracks":
tracks, err := m.commands.TrackList(m.ctx, (page + 1))
page++
if err != nil {
return
}
page++
items := []list.Item{}
for _, track := range tracks.Tracks {
items = append(items, mainItem{

View File

@ -5,9 +5,6 @@ import (
"strings"
"time"
"git.asdf.cafe/abs3nt/gospt/src/commands"
"git.asdf.cafe/abs3nt/gospt/src/gctx"
"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
@ -16,6 +13,9 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/zmb3/spotify/v2"
"git.asdf.cafe/abs3nt/gospt/src/commands"
"git.asdf.cafe/abs3nt/gospt/src/gctx"
)
var (
@ -25,30 +25,33 @@ var (
playbackContext string
main_updates chan *mainModel
page = 1
loading = false
showingMessage = false
)
type Mode string
const (
Album Mode = "album"
ArtistAlbum = "artistalbum"
Artist = "artist"
Artists = "artists"
Tracks = "tracks"
Albums = "albums"
Main = "main"
Playlists = "playlists"
Playlist = "playlist"
Devices = "devices"
Search = "search"
SearchAlbums = "searchalbums"
SearchAlbum = "searchalbum"
SearchArtists = "searchartists"
SearchArtist = "searchartist"
SearchArtistAlbum = "searchartistalbum"
SearchTracks = "searchtracks"
SearchPlaylists = "searchplaylsits"
SearchPlaylist = "searchplaylist"
ArtistAlbum Mode = "artistalbum"
Artist Mode = "artist"
Artists Mode = "artists"
Queue Mode = "queue"
Tracks Mode = "tracks"
Albums Mode = "albums"
Main Mode = "main"
Playlists Mode = "playlists"
Playlist Mode = "playlist"
Devices Mode = "devices"
Search Mode = "search"
SearchAlbums Mode = "searchalbums"
SearchAlbum Mode = "searchalbum"
SearchArtists Mode = "searchartists"
SearchArtist Mode = "searchartist"
SearchArtistAlbum Mode = "searchartistalbum"
SearchTracks Mode = "searchtracks"
SearchPlaylists Mode = "searchplaylsits"
SearchPlaylist Mode = "searchplaylist"
)
type mainItem struct {
@ -88,50 +91,55 @@ type mainModel struct {
}
func (m *mainModel) PlayRadio() {
m.list.NewStatusMessage("Starting radio for " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Starting radio for "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
selectedItem := m.list.SelectedItem().(mainItem).SpotifyItem
switch selectedItem.(type) {
switch item := selectedItem.(type) {
case spotify.SimplePlaylist:
go HandlePlaylistRadio(m.ctx, m.commands, selectedItem.(spotify.SimplePlaylist))
go HandlePlaylistRadio(m.ctx, m.commands, item)
return
case *spotify.SavedTrackPage:
go HandleLibraryRadio(m.ctx, m.commands)
return
case spotify.SimpleAlbum:
go HandleAlbumRadio(m.ctx, m.commands, selectedItem.(spotify.SimpleAlbum))
go HandleAlbumRadio(m.ctx, m.commands, item)
return
case spotify.FullAlbum:
go HandleAlbumRadio(m.ctx, m.commands, selectedItem.(spotify.FullAlbum).SimpleAlbum)
go HandleAlbumRadio(m.ctx, m.commands, item.SimpleAlbum)
return
case spotify.SimpleArtist:
go HandleArtistRadio(m.ctx, m.commands, selectedItem.(spotify.SimpleArtist))
go HandleArtistRadio(m.ctx, m.commands, item)
return
case spotify.FullArtist:
go HandleArtistRadio(m.ctx, m.commands, selectedItem.(spotify.FullArtist).SimpleArtist)
go HandleArtistRadio(m.ctx, m.commands, item.SimpleArtist)
return
case spotify.SimpleTrack:
go HandleRadio(m.ctx, m.commands, selectedItem.(spotify.SimpleTrack))
go HandleRadio(m.ctx, m.commands, item)
return
case spotify.FullTrack:
go HandleRadio(m.ctx, m.commands, selectedItem.(spotify.FullTrack).SimpleTrack)
go HandleRadio(m.ctx, m.commands, item.SimpleTrack)
return
case spotify.PlaylistTrack:
go HandleRadio(m.ctx, m.commands, selectedItem.(spotify.PlaylistTrack).Track.SimpleTrack)
go HandleRadio(m.ctx, m.commands, item.Track.SimpleTrack)
return
case spotify.PlaylistItem:
go HandleRadio(m.ctx, m.commands, item.Track.Track.SimpleTrack)
return
case spotify.SavedTrack:
go HandleRadio(m.ctx, m.commands, selectedItem.(spotify.SavedTrack).SimpleTrack)
go HandleRadio(m.ctx, m.commands, item.SimpleTrack)
return
}
}
func (m *mainModel) GoBack() (tea.Cmd, error) {
page = 1
switch m.mode {
case Main:
return tea.Quit, nil
case Albums, Artists, Tracks, Playlist, Devices, Search:
case Albums, Artists, Tracks, Playlist, Devices, Search, Queue:
m.mode = Main
new_items, err := MainView(m.ctx, m.commands)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case Album:
@ -203,48 +211,122 @@ type SpotifyUrl struct {
func (m *mainModel) CopyToClipboard() error {
item := m.list.SelectedItem().(mainItem).SpotifyItem
switch item.(type) {
switch converted := item.(type) {
case spotify.SimplePlaylist:
clipboard.WriteAll(item.(spotify.SimplePlaylist).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullPlaylist:
clipboard.WriteAll(item.(*spotify.FullPlaylist).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleAlbum:
clipboard.WriteAll(item.(spotify.SimpleAlbum).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullAlbum:
clipboard.WriteAll(item.(*spotify.FullAlbum).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleArtist:
clipboard.WriteAll(item.(spotify.SimpleArtist).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullArtist:
clipboard.WriteAll(item.(*spotify.FullArtist).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleTrack:
clipboard.WriteAll(item.(spotify.SimpleTrack).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.PlaylistTrack:
clipboard.WriteAll(item.(spotify.PlaylistTrack).Track.ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.Track.ExternalURLs["spotify"])
case spotify.SavedTrack:
clipboard.WriteAll(item.(spotify.SavedTrack).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.FullTrack:
clipboard.WriteAll(item.(spotify.FullTrack).ExternalURLs["spotify"])
m.list.NewStatusMessage("Copying link to " + m.list.SelectedItem().(mainItem).Title())
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
}
return nil
}
func (m *mainModel) SendMessage(msg string, duration time.Duration) {
showingMessage = true
defer func() {
showingMessage = false
}()
m.list.NewStatusMessage(msg)
time.Sleep(duration)
}
func (m *mainModel) QueueItem() error {
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)
case spotify.SavedTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.ctx, m.commands, item.ID)
case spotify.SimpleTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.ctx, m.commands, item.ID)
case spotify.FullTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.ctx, m.commands, item.ID)
case *spotify.FullTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.ctx, m.commands, item.ID)
case *spotify.SimpleTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.ctx, m.commands, item.ID)
case *spotify.SimplePlaylist:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.ctx, m.commands, item.ID)
}
if m.mode == Queue {
go func() {
new_items, err := QueueView(m.ctx, m.commands)
if err != nil {
return
}
m.list.SetItems(new_items)
}()
}
return nil
}
func (m *mainModel) DeleteTrackFromPlaylist() error {
if m.mode != Playlist {
return nil
}
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)
if err != nil {
return
}
m.list.SetItems(new_items)
}()
return nil
}
func (m *mainModel) SelectItem() error {
switch m.mode {
case Queue:
page = 1
go func() {
HandleNextInQueue(m.ctx, m.commands, m.list.Index())
new_items, err := QueueView(m.ctx, m.commands)
if err != nil {
return
}
m.list.SetItems(new_items)
m.list.ResetSelected()
}()
case Search:
switch m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
page = 1
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case *spotify.FullArtistPage:
m.mode = SearchArtists
new_items, err := SearchArtistsView(m.ctx, m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.FullArtistPage))
new_items, err := SearchArtistsView(m.ctx, m.commands, item)
if err != nil {
return err
}
@ -252,7 +334,7 @@ func (m *mainModel) SelectItem() error {
m.list.ResetSelected()
case *spotify.SimpleAlbumPage:
m.mode = SearchAlbums
new_items, err := SearchAlbumsView(m.ctx, m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.SimpleAlbumPage))
new_items, err := SearchAlbumsView(m.ctx, m.commands, item)
if err != nil {
return err
}
@ -260,8 +342,7 @@ func (m *mainModel) SelectItem() error {
m.list.ResetSelected()
case *spotify.SimplePlaylistPage:
m.mode = SearchPlaylists
playlists := m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.SimplePlaylistPage)
new_items, err := SearchPlaylistsView(m.ctx, m.commands, playlists)
new_items, err := SearchPlaylistsView(m.ctx, m.commands, item)
if err != nil {
return err
}
@ -269,7 +350,7 @@ func (m *mainModel) SelectItem() error {
m.list.ResetSelected()
case *spotify.FullTrackPage:
m.mode = SearchTracks
new_items, err := SearchTracksView(m.ctx, m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.FullTrackPage))
new_items, err := SearchTracksView(m.ctx, m.commands, item)
if err != nil {
return err
}
@ -277,6 +358,7 @@ func (m *mainModel) SelectItem() error {
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)
@ -286,6 +368,7 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
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)
@ -295,6 +378,7 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
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)
@ -304,6 +388,7 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
m.list.ResetSelected()
case SearchPlaylists:
page = 1
m.mode = SearchPlaylist
playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist)
m.playlist = playlist
@ -314,7 +399,16 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
m.list.ResetSelected()
case Main:
switch m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
page = 1
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case spotify.Queue:
m.mode = Queue
new_items, err := QueueView(m.ctx, m.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case *spotify.FullArtistCursorPage:
m.mode = Artists
new_items, err := ArtistsView(m.ctx, m.commands)
@ -333,9 +427,8 @@ func (m *mainModel) SelectItem() error {
m.list.ResetSelected()
case spotify.SimplePlaylist:
m.mode = Playlist
playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist)
m.playlist = playlist
new_items, err := PlaylistView(m.ctx, m.commands, playlist)
m.playlist = item
new_items, err := PlaylistView(m.ctx, m.commands, item)
if err != nil {
return err
}
@ -351,6 +444,7 @@ func (m *mainModel) SelectItem() error {
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)
@ -378,18 +472,19 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
m.list.ResetSelected()
case Album, ArtistAlbum, SearchArtistAlbum, SearchAlbum:
go HandlePlayWithContext(m.ctx, m.commands, &m.album.URI, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.TotalPages))
pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.TotalPages)
go HandlePlayWithContext(m.ctx, m.commands, &m.album.URI, &pos)
case Playlist, SearchPlaylist:
go HandlePlayWithContext(m.ctx, m.commands, &m.playlist.URI, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage))
pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.PerPage)
go HandlePlayWithContext(m.ctx, m.commands, &m.playlist.URI, &pos)
case Tracks:
go HandlePlayLikedSong(m.ctx, m.commands, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage))
case SearchTracks:
go HandlePlayTrack(m.ctx, m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.FullTrack).ID)
case Devices:
go HandleSetDevice(m.ctx, m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.PlayerDevice))
m.list.NewStatusMessage("Setting device to " + m.list.SelectedItem().FilterValue())
go m.SendMessage("Setting device to "+m.list.SelectedItem().FilterValue(), 2*time.Second)
m.mode = "main"
m.list.NewStatusMessage("Setting view to main")
new_items, err := MainView(m.ctx, m.commands)
if err != nil {
return err
@ -399,7 +494,7 @@ func (m *mainModel) SelectItem() error {
return nil
}
func (m mainModel) Init() tea.Cmd {
func (m *mainModel) Init() tea.Cmd {
main_updates = make(chan *mainModel)
return Tick()
}
@ -437,7 +532,7 @@ func (m *mainModel) TickPlayback() {
}()
}
func (m mainModel) View() string {
func (m *mainModel) View() string {
if m.input.Focused() {
return DocStyle.Render(m.list.View() + "\n" + m.input.View())
}
@ -497,7 +592,7 @@ func (m *mainModel) getContext(playing *spotify.CurrentlyPlaying) (string, error
return "", nil
}
func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Update list items from LoadMore
select {
case update := <-main_updates:
@ -505,7 +600,7 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
default:
}
// Call for more items if needed
if m.list.Paginator.Page == m.list.Paginator.TotalPages-2 && m.list.Cursor() == 0 {
if m.list.Paginator.Page == m.list.Paginator.TotalPages-1 && m.list.Cursor() == 0 && !loading {
// if last request was still full request more
if len(m.list.Items())%50 == 0 {
go m.LoadMoreItems()
@ -519,6 +614,17 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmd := m.progress.SetPercent(float64(playing.Progress) / float64(playing.Item.Duration))
m.playing = playing
m.playbackContext = playbackContext
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)
if err != nil {
return
}
m.list.SetItems(new_items)
}()
}
}
return m, tea.Batch(Tick(), cmd)
}
return m, Tick()
@ -526,15 +632,17 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
m.list.NewStatusMessage(
fmt.Sprintf("Now playing %s by %s - %s %s/%s : %s",
m.playing.Item.Name,
m.playing.Item.Artists[0].Name,
m.progress.View(),
(time.Duration(m.playing.Progress) * time.Millisecond).Round(time.Second),
(time.Duration(m.playing.Item.Duration) * time.Millisecond).Round(time.Second),
m.playbackContext),
)
if !showingMessage {
m.list.NewStatusMessage(
fmt.Sprintf("Now playing %s by %s - %s %s/%s : %s",
m.playing.Item.Name,
m.playing.Item.Artists[0].Name,
m.progress.View(),
(time.Duration(m.playing.Progress) * time.Millisecond).Round(time.Second),
(time.Duration(m.playing.Item.Duration) * time.Millisecond).Round(time.Second),
m.playbackContext),
)
}
return m, cmd
case tea.KeyMsg:
// quit
@ -580,7 +688,6 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.list.SetItems(new_items)
m.list.ResetSelected()
m.list.NewStatusMessage("Setting view to devices")
}
// go back
if msg.String() == "backspace" || msg.String() == "esc" || msg.String() == "q" {
@ -591,9 +698,20 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.list.ResetSelected()
return m, msg
}
if msg.String() == "ctrl+d" {
err := m.DeleteTrackFromPlaylist()
if err != nil {
return m, tea.Quit
}
}
if msg.String() == "ctrl+@" || msg.String() == "ctrl+p" {
err := m.QueueItem()
if err != nil {
return m, tea.Quit
}
}
// select item
if msg.String() == "enter" || msg.String() == "spacebar" {
if msg.String() == "enter" || msg.String() == " " || msg.String() == "p" {
err := m.SelectItem()
if err != nil {
return m, tea.Quit
@ -647,7 +765,7 @@ func InitMain(ctx *gctx.Context, c *commands.Commands, mode Mode) (tea.Model, er
return nil, err
}
}
m := mainModel{
m := &mainModel{
list: list.New(items, list.NewDefaultDelegate(), 0, 0),
ctx: ctx,
commands: c,
@ -658,27 +776,30 @@ func InitMain(ctx *gctx.Context, c *commands.Commands, mode Mode) (tea.Model, er
go m.TickPlayback()
Tick()
m.list.DisableQuitKeybindings()
m.list.SetFilteringEnabled(false)
m.list.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")),
key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "search")),
key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit")),
key.NewBinding(key.WithKeys("ctrl"+"r"), key.WithHelp("ctrl+r", "start radio")),
key.NewBinding(key.WithKeys("ctrl"+"shift"+"c"), key.WithHelp("ctrl+shift+c", "copy url")),
key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "back")),
key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "search")),
key.NewBinding(key.WithKeys("ctrl"+"r"), key.WithHelp("ctrl+r", "radio")),
key.NewBinding(key.WithKeys("ctrl"+"p"), key.WithHelp("ctrl+p", "queue")),
key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "select device")),
}
}
m.list.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")),
key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "search")),
key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "search")),
key.NewBinding(key.WithKeys(">"), key.WithHelp(">", "seek forward")),
key.NewBinding(key.WithKeys("<"), key.WithHelp("<", "seek backward")),
key.NewBinding(key.WithKeys("+"), key.WithHelp("+", "volume up")),
key.NewBinding(key.WithKeys("-"), key.WithHelp("-", "volume down")),
key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy link to item")),
key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit")),
key.NewBinding(key.WithKeys("ctrl"+"r"), key.WithHelp("ctrl+r", "start radio")),
key.NewBinding(key.WithKeys("ctrl"+"shift"+"c"), key.WithHelp("ctrl+shift+c", "copy url")),
key.NewBinding(key.WithKeys("ctrl"+"p"), key.WithHelp("ctrl+p", "queue song")),
key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "select device")),
}
}

View File

@ -1,8 +1,6 @@
package tui
import (
"fmt"
"git.asdf.cafe/abs3nt/gospt/src/commands"
"git.asdf.cafe/abs3nt/gospt/src/gctx"
@ -13,10 +11,10 @@ import (
func StartTea(ctx *gctx.Context, cmd *commands.Commands, mode string) error {
m, err := InitMain(ctx, cmd, Mode(mode))
if err != nil {
fmt.Println("UH OH")
return err
}
P = tea.NewProgram(m, tea.WithAltScreen())
if err := P.Start(); err != nil {
if _, err := P.Run(); err != nil {
return err
}
return nil

View File

@ -2,16 +2,19 @@ package tui
import (
"fmt"
"sync"
"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"
)
const regex = `<.*?>`
func DeviceView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
items := []list.Item{}
devices, err := commands.Client().PlayerDevices(ctx)
@ -28,25 +31,54 @@ func DeviceView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, er
return items, nil
}
func PlaylistView(ctx *gctx.Context, commands *commands.Commands, playlist spotify.SimplePlaylist) ([]list.Item, error) {
func QueueView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
items := []list.Item{}
tracks, err := commands.PlaylistTracks(ctx, playlist.ID, 1)
tracks, err := commands.UserQueue(ctx)
if err != nil {
return nil, err
}
for _, track := range tracks.Tracks {
if tracks.CurrentlyPlaying.Name != "" {
items = append(items, mainItem{
Name: track.Track.Name,
Artist: track.Track.Artists[0],
Duration: track.Track.TimeDuration().Round(time.Second).String(),
ID: track.Track.ID,
Desc: track.Track.Artists[0].Name + " - " + track.Track.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,
})
}
for _, track := range tracks.Items {
items = append(items, mainItem{
Name: track.Name,
Artist: track.Artists[0],
Duration: track.TimeDuration().Round(time.Second).String(),
ID: track.ID,
Desc: track.Artists[0].Name + " - " + track.TimeDuration().Round(time.Second).String(),
SpotifyItem: track,
})
}
return items, nil
}
func PlaylistView(ctx *gctx.Context, commands *commands.Commands, playlist spotify.SimplePlaylist) ([]list.Item, error) {
items := []list.Item{}
playlistItems, err := commands.PlaylistTracks(ctx, playlist.ID, 1)
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(),
SpotifyItem: item,
})
}
return items, nil
}
func ArtistsView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
items := []list.Item{}
artists, err := commands.UserArtists(ctx, 1)
@ -57,7 +89,7 @@ func ArtistsView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, e
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),
Desc: fmt.Sprintf("%d followers", artist.Followers.Count),
SpotifyItem: artist.SimpleArtist,
})
}
@ -70,7 +102,7 @@ func SearchArtistsView(ctx *gctx.Context, commands *commands.Commands, 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),
Desc: fmt.Sprintf("%d followers", artist.Followers.Count),
SpotifyItem: artist.SimpleArtist,
})
}
@ -84,27 +116,13 @@ func SearchView(ctx *gctx.Context, commands *commands.Commands, search string) (
if err != nil {
return nil, nil, err
}
items = append(items, mainItem{
Name: "Tracks",
Desc: "Search results",
SpotifyItem: result.Tracks,
})
items = append(items, mainItem{
Name: "Albums",
Desc: "Search results",
SpotifyItem: result.Albums,
})
items = append(items, mainItem{
Name: "Artists",
Desc: "Search results",
SpotifyItem: result.Artists,
})
items = append(items, mainItem{
Name: "Playlists",
Desc: "Search results",
SpotifyItem: result.Playlists,
})
items = append(
items,
mainItem{Name: "Tracks", Desc: "Search results", SpotifyItem: result.Tracks},
mainItem{Name: "Albums", Desc: "Search results", SpotifyItem: result.Albums},
mainItem{Name: "Artists", Desc: "Search results", SpotifyItem: result.Artists},
mainItem{Name: "Playlists", Desc: "Search results", SpotifyItem: result.Playlists},
)
results := &SearchResults{
Tracks: result.Tracks,
Playlists: result.Playlists,
@ -124,7 +142,7 @@ func AlbumsView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, er
items = append(items, mainItem{
Name: album.Name,
ID: album.ID,
Desc: fmt.Sprintf("%s, %d tracks", album.Artists[0].Name, album.Tracks.Total),
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,
})
}
@ -136,7 +154,7 @@ func SearchPlaylistsView(ctx *gctx.Context, commands *commands.Commands, playlis
for _, playlist := range playlists.Playlists {
items = append(items, mainItem{
Name: playlist.Name,
Desc: playlist.Description,
Desc: stripHtmlRegex(playlist.Description),
SpotifyItem: playlist,
})
}
@ -149,7 +167,7 @@ func SearchAlbumsView(ctx *gctx.Context, commands *commands.Commands, albums *sp
items = append(items, mainItem{
Name: album.Name,
ID: album.ID,
Desc: fmt.Sprintf("%s, %d", album.Artists[0].Name, album.ReleaseDateTime()),
Desc: fmt.Sprintf("%s by %s, released %d", album.AlbumType, album.Artists[0].Name, album.ReleaseDateTime().Year()),
SpotifyItem: album,
})
}
@ -227,57 +245,36 @@ func SavedTracksView(ctx *gctx.Context, commands *commands.Commands) ([]list.Ite
}
func MainView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, error) {
var wg sync.WaitGroup
wg := errgroup.Group{}
var saved_items *spotify.SavedTrackPage
var playlists *spotify.SimplePlaylistPage
var artists *spotify.FullArtistCursorPage
var albums *spotify.SavedAlbumPage
wg.Add(1)
go func() {
defer wg.Done()
var err error
wg.Go(func() (err error) {
saved_items, err = commands.TrackList(ctx, 1)
if err != nil {
fmt.Println(err.Error())
return
}
}()
return
})
wg.Add(1)
go func() {
defer wg.Done()
var err error
wg.Go(func() (err error) {
playlists, err = commands.Playlists(ctx, 1)
if err != nil {
fmt.Println(err.Error())
return
}
}()
return
})
wg.Add(1)
go func() {
defer wg.Done()
var err error
wg.Go(func() (err error) {
artists, err = commands.UserArtists(ctx, 1)
if err != nil {
fmt.Println(err.Error())
return
}
}()
return
})
wg.Add(1)
go func() {
defer wg.Done()
var err error
wg.Go(func() (err error) {
albums, err = commands.UserAlbums(ctx, 1)
if err != nil {
fmt.Println(err.Error())
return
}
}()
return
})
wg.Wait()
err := wg.Wait()
if err != nil {
return nil, err
}
items := []list.Item{}
if saved_items != nil && saved_items.Total != 0 {
@ -301,14 +298,24 @@ func MainView(ctx *gctx.Context, commands *commands.Commands) ([]list.Item, erro
SpotifyItem: artists,
})
}
items = append(items, mainItem{
Name: "Queue",
Desc: "Your Current Queue",
SpotifyItem: spotify.Queue{},
})
if playlists != nil && playlists.Total != 0 {
for _, playlist := range playlists.Playlists {
items = append(items, mainItem{
Name: playlist.Name,
Desc: playlist.Description,
Desc: stripHtmlRegex(playlist.Description),
SpotifyItem: playlist,
})
}
}
return items, nil
}
func stripHtmlRegex(s string) string {
r := regexp.MustCompile(regex)
return r.ReplaceAllString(s, "")
}

115
src/youtube/youtube.go Normal file
View File

@ -0,0 +1,115 @@
package youtube
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/user"
"path/filepath"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
)
func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
cacheFile, err := tokenCacheFile()
if err != nil {
log.Fatalf("Unable to get path to cached credential file. %v", err)
}
tok, err := tokenFromFile(cacheFile)
if err != nil {
tok = getTokenFromWeb(config)
saveToken(cacheFile, tok)
}
return config.Client(ctx, tok)
}
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
fmt.Printf("Go to the following link in your browser then type the "+
"authorization code: \n%v\n", authURL)
var code string
if _, err := fmt.Scan(&code); err != nil {
log.Fatalf("Unable to read authorization code %v", err)
}
tok, err := config.Exchange(context.Background(), code)
if err != nil {
log.Fatalf("Unable to retrieve token from web %v", err)
}
return tok
}
func tokenCacheFile() (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
err = os.MkdirAll(tokenCacheDir, 0o700)
if err != nil {
return "", err
}
return filepath.Join(tokenCacheDir,
url.QueryEscape("youtube-go-quickstart.json")), err
}
func tokenFromFile(file string) (*oauth2.Token, error) {
f, err := os.Open(file)
handleError(err, "Error opening file")
t := &oauth2.Token{}
err = json.NewDecoder(f).Decode(t)
defer f.Close()
return t, err
}
func saveToken(file string, token *oauth2.Token) {
fmt.Printf("Saving credential file to: %s\n", file)
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
log.Fatalf("Unable to cache oauth token: %v", err)
}
defer f.Close()
err = json.NewEncoder(f).Encode(token)
handleError(err, "Error encoding token")
}
func handleError(err error, message string) {
if message == "" {
message = "Error making API call"
}
if err != nil {
log.Fatalf(message+": %v", err.Error())
}
}
func Search(query string) string {
ctx := context.Background()
confDir, _ := os.UserConfigDir()
b, err := os.ReadFile(filepath.Join(confDir, "gospt", "client_secret.json"))
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
config, err := google.ConfigFromJSON(b, youtube.YoutubeReadonlyScope)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
client := getClient(ctx, config)
service, err := youtube.NewService(ctx, option.WithHTTPClient(client))
handleError(err, "Error creating YouTube client")
call := service.Search.List([]string{"snippet"})
call.Q(query)
response, err := call.Do()
handleError(err, "")
return fmt.Sprintf("https://www.youtube.com/watch?v=%s", response.Items[0].Id.VideoId)
}