tui exists again

This commit is contained in:
abs3nt 2024-02-18 15:04:34 -08:00
parent fa06934f31
commit 3b8a51db39
Signed by: abs3nt
GPG Key ID: A7BD96A8BAB04C09
18 changed files with 2092 additions and 33 deletions

48
go.mod
View File

@ -5,48 +5,73 @@ go 1.22.0
require (
gfx.cafe/util/go/frand v0.0.0-20231226111635-bc00a6a250fb
gfx.cafe/util/go/fxplus v0.0.0-20231226111635-bc00a6a250fb
git.asdf.cafe/abs3nt/gospt v0.0.52
git.asdf.cafe/abs3nt/gunner v0.0.1
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/lmittmann/tint v1.0.4
github.com/urfave/cli/v2 v2.27.1
github.com/zmb3/spotify/v2 v2.4.1
go.uber.org/fx v1.20.1
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5
google.golang.org/api v0.30.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sync v0.3.0
google.golang.org/api v0.133.0
modernc.org/sqlite v1.29.1
)
require (
cloud.google.com/go v0.65.0 // indirect
cloud.google.com/go/compute v1.22.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // 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.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cristalhq/aconfig v0.18.5 // indirect
github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.16 // 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.19 // indirect
github.com/mattn/go-localereader v0.0.1 // 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.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-0.20240127222946-601bbb3750c2 // indirect
github.com/rs/zerolog v1.29.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.22.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
google.golang.org/grpc v1.31.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 // indirect
google.golang.org/grpc v1.56.2 // indirect
google.golang.org/protobuf v1.31.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
@ -54,4 +79,5 @@ require (
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
tuxpa.in/a/zlog v1.61.0 // indirect
)

143
go.sum
View File

@ -12,7 +12,6 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
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 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
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=
@ -20,6 +19,10 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
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.22.0 h1:cB8R6FtUtT1TYGl5R3xuxnW6OUIc/DrT2aiR16TTG7Y=
cloud.google.com/go/compute v1.22.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
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/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=
@ -36,20 +39,45 @@ gfx.cafe/util/go/frand v0.0.0-20231226111635-bc00a6a250fb h1:sBsE/GNN43F9a9/FQjb
gfx.cafe/util/go/frand v0.0.0-20231226111635-bc00a6a250fb/go.mod h1:LNHxMJl0WnIr5+OChYxlVopxk+j7qxZv0XvWCzB6uGE=
gfx.cafe/util/go/fxplus v0.0.0-20231226111635-bc00a6a250fb h1:JL2ZB1wCxGS/mBIsTwDrgGEndHVQtZuba9dgSk+OsSE=
gfx.cafe/util/go/fxplus v0.0.0-20231226111635-bc00a6a250fb/go.mod h1:qcgf/NcKZwJCETErwNtofMa10hQtP28ec1bN2nl8ahA=
git.asdf.cafe/abs3nt/gospt v0.0.52 h1:DlVUXT/KXV9HW2dJyFvijhjr7Ak8zIC2/4rUvjESmpA=
git.asdf.cafe/abs3nt/gospt v0.0.52/go.mod h1:8IqF8S5+ZjDfbmBrXqff542kYu/1t1h+Qin89i1ZNSA=
git.asdf.cafe/abs3nt/gunner v0.0.1 h1:N6kCe7fH83kzm1Sjp/5uZbl8FM5s7KoYCfmhO8qyQbA=
git.asdf.cafe/abs3nt/gunner v0.0.1/go.mod h1:Q4zhiPfmffCVAb5xIzZn6Momm91uf/deqRVd2/vdjd4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
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/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/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/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
@ -65,15 +93,20 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
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/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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/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 h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
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=
@ -94,9 +127,11 @@ 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/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=
@ -106,6 +141,7 @@ 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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -121,11 +157,18 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
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/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
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=
@ -141,28 +184,67 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2 h1:tcc3ZFBvjydcgrAxavZRYqFqCKzy0FJ+UY4ATq4QVXk=
github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
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/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
@ -179,8 +261,10 @@ 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 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
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/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI=
@ -199,6 +283,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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=
@ -262,7 +348,9 @@ 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-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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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=
@ -273,8 +361,9 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
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.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
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=
@ -285,6 +374,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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=
@ -310,12 +401,19 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
@ -324,13 +422,16 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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/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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
@ -401,8 +502,9 @@ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
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 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.133.0 h1:N7Ym5Hl0Dpn0I0o7R1z4UpVA1GCDyS8vbPu1/ObV73A=
google.golang.org/api v0.133.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk=
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=
@ -434,13 +536,19 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
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 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8=
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 h1:co8AMhI481nhd3WBfW2mq5msyQHNBcGn7G9GCEqz45k=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
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=
@ -452,8 +560,13 @@ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
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 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
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=
@ -466,13 +579,15 @@ 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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -502,3 +617,5 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
tuxpa.in/a/zlog v1.61.0 h1:7wrS6G4QwpnOmgHRQknrr7IgiMXrfGpekkU0PjM9FhE=
tuxpa.in/a/zlog v1.61.0/go.mod h1:CNpMe8laDHLSypx/DyxfX1S0oyxUydeo3aGTEbtRBhg=

View File

@ -10,6 +10,7 @@ import (
"go.uber.org/fx"
"git.asdf.cafe/abs3nt/gospt-ng/src/components/commands"
"git.asdf.cafe/abs3nt/gospt-ng/src/components/tui"
)
var Version = "dev"
@ -291,6 +292,13 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
},
Category: "Playback",
},
{
Name: "tui",
Usage: "Starts the TUI",
Action: func(ctx *cli.Context) error {
return tui.StartTea(c, "main")
},
},
{
Name: "seek",
Usage: "Seek to a position in the song",

View File

@ -1,7 +1,6 @@
package commands
import (
"context"
"encoding/json"
"io"
"os"
@ -10,7 +9,7 @@ import (
"github.com/zmb3/spotify/v2"
)
func (c *Commander) activateDevice(ctx context.Context) (spotify.ID, error) {
func (c *Commander) activateDevice() (spotify.ID, error) {
var device *spotify.PlayerDevice
configDir, _ := os.UserConfigDir()
if _, err := os.Stat(filepath.Join(configDir, "gospt/device.json")); err == nil {
@ -27,7 +26,7 @@ func (c *Commander) activateDevice(ctx context.Context) (spotify.ID, error) {
if err != nil {
return "", err
}
err = c.Client.TransferPlayback(ctx, device.ID, true)
err = c.Client.TransferPlayback(c.Context, device.ID, true)
if err != nil {
return "", err
}

View File

@ -0,0 +1,18 @@
package commands
import (
"github.com/zmb3/spotify/v2"
)
func (c *Commander) AlbumTracks(album spotify.ID, page int) (*spotify.SimpleTrackPage, error) {
tracks, err := c.Client.
GetAlbumTracks(c.Context, album, spotify.Limit(50), spotify.Offset((page-1)*50), spotify.Market(spotify.CountryUSA))
if err != nil {
return nil, err
}
return tracks, nil
}
func (c *Commander) UserAlbums(page int) (*spotify.SavedAlbumPage, error) {
return c.Client.CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
}

View File

@ -0,0 +1,12 @@
package commands
import "github.com/zmb3/spotify/v2"
func (c *Commander) ArtistAlbums(artist spotify.ID, page int) (*spotify.SimpleAlbumPage, error) {
albums, err := c.Client.
GetArtistAlbums(c.Context, artist, []spotify.AlbumType{1, 2, 3, 4}, spotify.Market(spotify.CountryUSA), spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return nil, err
}
return albums, nil
}

View File

@ -8,7 +8,7 @@ func (c *Commander) Next() error {
err := c.Client.Next(c.Context)
if err != nil {
if isNoActiveError(err) {
deviceId, err := c.activateDevice(c.Context)
deviceId, err := c.activateDevice()
if err != nil {
return err
}

View File

@ -11,7 +11,7 @@ func (c *Commander) Play() error {
err := c.Client.Play(c.Context)
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice(c.Context)
deviceID, err := c.activateDevice()
if err != nil {
return err
}
@ -28,6 +28,63 @@ func (c *Commander) Play() error {
return nil
}
func (c *Commander) PlayLikedSongs(position int) error {
err := c.ClearRadio()
if err != nil {
return err
}
playlist, _, err := c.GetRadioPlaylist("Saved Songs")
if err != nil {
return err
}
songs, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(position))
if err != nil {
return err
}
to_add := []spotify.ID{}
for _, song := range songs.Tracks {
to_add = append(to_add, song.ID)
}
_, err = c.Client.AddTracksToPlaylist(c.Context, playlist.ID, to_add...)
if err != nil {
return err
}
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &playlist.URI,
})
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice()
if err != nil {
return err
}
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &playlist.URI,
DeviceID: &deviceID,
})
if err != nil {
return err
}
}
}
for page := 2; page <= 5; page++ {
songs, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((50*(page-1))+position))
if err != nil {
return err
}
to_add := []spotify.ID{}
for _, song := range songs.Tracks {
to_add = append(to_add, song.ID)
}
_, err = c.Client.AddTracksToPlaylist(c.Context, playlist.ID, to_add...)
if err != nil {
return err
}
}
return err
}
func (c *Commander) PlayUrl(urlString string) error {
url, err := url.Parse(urlString)
if err != nil {
@ -37,7 +94,7 @@ func (c *Commander) PlayUrl(urlString string) error {
err = c.Client.QueueSong(c.Context, spotify.ID(track_id))
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice(c.Context)
deviceID, err := c.activateDevice()
if err != nil {
return err
}
@ -64,3 +121,46 @@ func (c *Commander) PlayUrl(urlString string) error {
}
return nil
}
func (c *Commander) PlaySongInPlaylist(context *spotify.URI, offset *int) error {
e := c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
PlaybackContext: context,
})
if e != nil {
if isNoActiveError(e) {
deviceID, err := c.activateDevice()
if err != nil {
return err
}
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
PlaybackContext: context,
DeviceID: &deviceID,
})
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice()
if err != nil {
return err
}
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
PlaybackContext: context,
DeviceID: &deviceID,
})
if err != nil {
return err
}
}
}
err = c.Client.Play(c.Context)
if err != nil {
return err
}
} else {
return e
}
}
return nil
}

View File

@ -0,0 +1,23 @@
package commands
import "github.com/zmb3/spotify/v2"
func (c *Commander) Playlists(page int) (*spotify.SimplePlaylistPage, error) {
return c.Client.CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
}
func (c *Commander) PlaylistTracks(playlist spotify.ID, page int) (*spotify.PlaylistItemPage, error) {
return c.Client.GetPlaylistItems(c.Context, playlist, spotify.Limit(50), spotify.Offset((page-1)*50))
}
func (c *Commander) DeleteTracksFromPlaylist(tracks []spotify.ID, playlist spotify.ID) error {
_, err := c.Client.RemoveTracksFromPlaylist(c.Context, playlist, tracks...)
if err != nil {
return err
}
return nil
}
func (c *Commander) TrackList(page int) (*spotify.SavedTrackPage, error) {
return c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
}

View File

@ -8,7 +8,7 @@ func (c *Commander) Previous() error {
err := c.Client.Previous(c.Context)
if err != nil {
if isNoActiveError(err) {
deviceId, err := c.activateDevice(c.Context)
deviceId, err := c.activateDevice()
if err != nil {
return err
}

View File

@ -0,0 +1,25 @@
package commands
import "github.com/zmb3/spotify/v2"
func (c *Commander) QueueSong(id spotify.ID) error {
err := c.Client.QueueSong(c.Context, id)
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice()
if err != nil {
return err
}
err = c.Client.QueueSongOpt(c.Context, id, &spotify.PlayOptions{
DeviceID: &deviceID,
})
if err != nil {
return err
}
return nil
} else {
return err
}
}
return nil
}

View File

@ -23,7 +23,7 @@ func (c *Commander) Radio() error {
if current_song.Item != nil {
return c.RadioGivenSong(current_song.Item.SimpleTrack, current_song.Progress)
}
_, err = c.activateDevice(c.Context)
_, err = c.activateDevice()
if err != nil {
return err
}
@ -34,6 +34,146 @@ func (c *Commander) Radio() error {
return c.RadioGivenSong(tracks.Tracks[frand.Intn(len(tracks.Tracks))].SimpleTrack, 0)
}
func (c *Commander) RadioFromPlaylist(playlist spotify.SimplePlaylist) error {
total := playlist.Tracks.Total
if total == 0 {
return fmt.Errorf("this playlist is empty")
}
pages := int(math.Ceil(float64(total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(pages-1) + 1
}
playlistPage, err := c.Client.GetPlaylistItems(c.Context, 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] })
seedCount := 5
if len(pageSongs) < seedCount {
seedCount = len(pageSongs)
}
seedIds := []spotify.ID{}
for idx, song := range pageSongs {
if idx >= seedCount {
break
}
seedIds = append(seedIds, song.Track.Track.ID)
}
return c.RadioGivenList(seedIds[:seedCount], playlist.Name)
}
func (c *Commander) RadioFromSavedTracks() error {
savedSongs, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0))
if err != nil {
return err
}
if savedSongs.Total == 0 {
return fmt.Errorf("you have no saved songs")
}
pages := int(math.Ceil(float64(savedSongs.Total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(pages-1) + 1
}
trackPage, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(randomPage*50))
if err != nil {
return err
}
pageSongs := trackPage.Tracks
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
seedCount := 4
seedIds := []spotify.ID{}
for idx, song := range pageSongs {
if idx >= seedCount {
break
}
seedIds = append(seedIds, song.ID)
}
seedIds = append(seedIds, savedSongs.Tracks[0].ID)
return c.RadioGivenList(seedIds, "Saved Tracks")
}
func (c *Commander) RadioGivenArtist(artist spotify.SimpleArtist) error {
seed := spotify.Seeds{
Artists: []spotify.ID{artist.ID},
}
recomendations, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil {
return err
}
recomendationIds := []spotify.ID{}
for _, song := range recomendations.Tracks {
recomendationIds = append(recomendationIds, song.ID)
}
err = c.ClearRadio()
if err != nil {
return err
}
radioPlaylist, db, err := c.GetRadioPlaylist(artist.Name)
if err != nil {
return err
}
queue := []spotify.ID{}
for _, rec := range recomendationIds {
exists, err := c.SongExists(db, rec)
if err != nil {
return err
}
if !exists {
_, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
if err != nil {
return err
}
queue = append(queue, rec)
}
}
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
if err != nil {
return err
}
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
})
if err != nil {
return err
}
err = c.Client.Repeat(c.Context, "context")
if err != nil {
return err
}
for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]},
}
additional_recs, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil {
return err
}
additionalRecsIds := []spotify.ID{}
for _, song := range additional_recs.Tracks {
exists, err := c.SongExists(db, song.ID)
if err != nil {
return err
}
if !exists {
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
if err != nil {
return err
}
additionalRecsIds = append(additionalRecsIds, song.ID)
}
}
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
if err != nil {
return err
}
}
return nil
}
func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos int) error {
start := time.Now().UnixMilli()
seed := spotify.Seeds{
@ -87,7 +227,7 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos int) error {
})
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice(c.Context)
deviceID, err := c.activateDevice()
if err != nil {
return err
}
@ -368,3 +508,124 @@ func (c *Commander) RefillRadio() error {
}
return nil
}
func (c *Commander) RadioFromAlbum(album spotify.SimpleAlbum) error {
tracks, err := c.AlbumTracks(album.ID, 1)
if err != nil {
return err
}
total := tracks.Total
if total == 0 {
return fmt.Errorf("this playlist is empty")
}
pages := int(math.Ceil(float64(total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(pages-1) + 1
}
albumTrackPage, err := c.AlbumTracks(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] })
seedCount := 5
if len(pageSongs) < seedCount {
seedCount = len(pageSongs)
}
seedIds := []spotify.ID{}
for idx, song := range pageSongs {
if idx >= seedCount {
break
}
seedIds = append(seedIds, song.ID)
}
return c.RadioGivenList(seedIds[:seedCount], album.Name)
}
func (c *Commander) RadioGivenList(song_ids []spotify.ID, name string) error {
seed := spotify.Seeds{
Tracks: song_ids,
}
recomendations, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
if err != nil {
return err
}
recomendationIds := []spotify.ID{}
for _, song := range recomendations.Tracks {
recomendationIds = append(recomendationIds, song.ID)
}
err = c.ClearRadio()
if err != nil {
return err
}
radioPlaylist, db, err := c.GetRadioPlaylist(name)
if err != nil {
return err
}
queue := []spotify.ID{song_ids[0]}
for _, rec := range recomendationIds {
exists, err := c.SongExists(db, rec)
if err != nil {
return err
}
if !exists {
_, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
if err != nil {
return err
}
queue = append(queue, rec)
}
}
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
if err != nil {
return err
}
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
})
if err != nil {
if isNoActiveError(err) {
deviceId, err := c.activateDevice()
if err != nil {
return err
}
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
DeviceID: &deviceId,
})
if err != nil {
return err
}
}
}
for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]},
}
additional_recs, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil {
return err
}
additionalRecsIds := []spotify.ID{}
for _, song := range additional_recs.Tracks {
exists, err := c.SongExists(db, song.ID)
if err != nil {
return err
}
if !exists {
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
if err != nil {
return err
}
additionalRecsIds = append(additionalRecsIds, song.ID)
}
}
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,12 @@
package commands
import "github.com/zmb3/spotify/v2"
func (c *Commander) Search(search string, page int) (*spotify.SearchResult, error) {
result, err := c.Client.
Search(c.Context, search, spotify.SearchTypeAlbum|spotify.SearchTypeArtist|spotify.SearchTypeTrack|spotify.SearchTypePlaylist, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return nil, err
}
return result, nil
}

View File

@ -0,0 +1,120 @@
package tui
import (
"github.com/zmb3/spotify/v2"
"git.asdf.cafe/abs3nt/gospt-ng/src/components/commands"
)
func HandlePlayWithContext(commands *commands.Commander, uri *spotify.URI, pos *int) {
err := commands.PlaySongInPlaylist(uri, pos)
if err != nil {
return
}
}
func HandleRadio(commands *commands.Commander, song spotify.SimpleTrack) {
err := commands.RadioGivenSong(song, 0)
if err != nil {
return
}
}
func HandleAlbumRadio(commands *commands.Commander, album spotify.SimpleAlbum) {
err := commands.RadioFromAlbum(album)
if err != nil {
return
}
}
func HandleSeek(commands *commands.Commander, fwd bool) {
err := commands.Seek(fwd)
if err != nil {
return
}
}
func HandleVolume(commands *commands.Commander, up bool) {
vol := 10
if !up {
vol = -10
}
err := commands.ChangeVolume(vol)
if err != nil {
return
}
}
func HandleArtistRadio(commands *commands.Commander, artist spotify.SimpleArtist) {
err := commands.RadioGivenArtist(artist)
if err != nil {
return
}
}
func HandleAlbumArtist(commands *commands.Commander, artist spotify.SimpleArtist) {
err := commands.RadioGivenArtist(artist)
if err != nil {
return
}
}
func HandlePlaylistRadio(commands *commands.Commander, playlist spotify.SimplePlaylist) {
err := commands.RadioFromPlaylist(playlist)
if err != nil {
return
}
}
func HandleLibraryRadio(commands *commands.Commander) {
err := commands.RadioFromSavedTracks()
if err != nil {
return
}
}
func HandlePlayLikedSong(commands *commands.Commander, position int) {
err := commands.PlayLikedSongs(position)
if err != nil {
return
}
}
func HandlePlayTrack(commands *commands.Commander, track spotify.ID) {
err := commands.QueueSong(track)
if err != nil {
return
}
err = commands.Next()
if err != nil {
return
}
}
func HandleNextInQueue(commands *commands.Commander, amt int) {
err := commands.Next()
if err != nil {
return
}
}
func HandleQueueItem(commands *commands.Commander, item spotify.ID) {
err := commands.QueueSong(item)
if err != nil {
return
}
}
func HandleDeleteTrackFromPlaylist(commands *commands.Commander, item, playlist spotify.ID) {
err := commands.DeleteTracksFromPlaylist([]spotify.ID{item}, playlist)
if err != nil {
return
}
}
func HandleSetDevice(commands *commands.Commander, player spotify.PlayerDevice) {
err := commands.SetDevice(player.ID)
if err != nil {
return
}
}

View File

@ -0,0 +1,165 @@
package tui
import (
"fmt"
"time"
"github.com/charmbracelet/bubbles/list"
"github.com/zmb3/spotify/v2"
)
func (m *mainModel) LoadMoreItems() {
loading = true
defer func() {
page++
loading = false
}()
switch m.mode {
case "artist":
albums, err := m.commands.ArtistAlbums(m.artist.ID, (page + 1))
if err != nil {
return
}
items := []list.Item{}
for _, album := range albums.Albums {
items = append(items, mainItem{
Name: album.Name,
ID: album.ID,
Desc: fmt.Sprintf("%s by %s", album.AlbumType, album.Artists[0].Name),
SpotifyItem: album,
})
}
for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item)
}
main_updates <- m
return
case "artists":
artists, err := m.commands.Client.CurrentUsersFollowedArtists(
m.commands.Context,
spotify.Limit(50),
spotify.Offset((page)*50),
)
if err != nil {
return
}
items := []list.Item{}
for _, artist := range artists.Artists {
items = append(items, mainItem{
Name: artist.Name,
ID: artist.ID,
Desc: fmt.Sprintf(
"%d followers, genres: %s, popularity: %d",
artist.Followers.Count,
artist.Genres,
artist.Popularity,
),
SpotifyItem: artist.SimpleArtist,
})
}
for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item)
}
main_updates <- m
return
case "album":
tracks, err := m.commands.AlbumTracks(m.album.ID, (page + 1))
if err != nil {
return
}
items := []mainItem{}
for _, track := range tracks.Tracks {
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(),
})
}
for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item)
}
main_updates <- m
return
case "albums":
albums, err := m.commands.UserAlbums(page + 1)
if err != nil {
return
}
items := []list.Item{}
for _, album := range albums.Albums {
items = append(items, mainItem{
Name: album.Name,
ID: album.ID,
Desc: fmt.Sprintf("%s, %d tracks", album.Artists[0].Name, album.Tracks.Total),
SpotifyItem: album.SimpleAlbum,
})
}
for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item)
}
main_updates <- m
return
case "main":
playlists, err := m.commands.Playlists(page + 1)
if err != nil {
return
}
items := []list.Item{}
for _, playlist := range playlists.Playlists {
items = append(items, mainItem{
Name: playlist.Name,
Desc: playlist.Description,
SpotifyItem: playlist,
})
}
for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item)
}
main_updates <- m
return
case "playlist":
playlistItems, err := m.commands.PlaylistTracks(m.playlist.ID, (page + 1))
if err != nil {
return
}
items := []mainItem{}
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(),
})
}
for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item)
}
main_updates <- m
return
case "tracks":
tracks, err := m.commands.TrackList(page + 1)
if err != nil {
return
}
items := []list.Item{}
for _, track := range tracks.Tracks {
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(),
})
}
for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item)
}
main_updates <- m
return
}
}

810
src/components/tui/main.go Normal file
View File

@ -0,0 +1,810 @@
package tui
import (
"fmt"
"strings"
"time"
"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/zmb3/spotify/v2"
"git.asdf.cafe/abs3nt/gospt-ng/src/components/commands"
)
var (
P *tea.Program
DocStyle = lipgloss.NewStyle().Margin(0, 2).Border(lipgloss.DoubleBorder(), true, true, true, true)
currentlyPlaying *spotify.CurrentlyPlaying
playbackContext string
main_updates chan *mainModel
page = 1
loading = false
showingMessage = false
)
type Mode string
const (
Album Mode = "album"
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 {
Name string
Duration string
Artist spotify.SimpleArtist
ID spotify.ID
Desc string
SpotifyItem any
}
type SearchResults struct {
Tracks *spotify.FullTrackPage
Artists *spotify.FullArtistPage
Playlists *spotify.SimplePlaylistPage
Albums *spotify.SimpleAlbumPage
}
func (i mainItem) Title() string { return i.Name }
func (i mainItem) Description() string { return i.Desc }
func (i mainItem) FilterValue() string { return i.Title() + i.Desc }
type mainModel struct {
list list.Model
input textinput.Model
commands *commands.Commander
mode Mode
playlist spotify.SimplePlaylist
artist spotify.SimpleArtist
album spotify.SimpleAlbum
searchResults *SearchResults
progress progress.Model
playing *spotify.CurrentlyPlaying
playbackContext string
search string
}
func (m *mainModel) PlayRadio() {
go m.SendMessage("Starting radio for "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
selectedItem := m.list.SelectedItem().(mainItem).SpotifyItem
switch item := selectedItem.(type) {
case spotify.SimplePlaylist:
go HandlePlaylistRadio(m.commands, item)
return
case *spotify.SavedTrackPage:
go HandleLibraryRadio(m.commands)
return
case spotify.SimpleAlbum:
go HandleAlbumRadio(m.commands, item)
return
case spotify.FullAlbum:
go HandleAlbumRadio(m.commands, item.SimpleAlbum)
return
case spotify.SimpleArtist:
go HandleArtistRadio(m.commands, item)
return
case spotify.FullArtist:
go HandleArtistRadio(m.commands, item.SimpleArtist)
return
case spotify.SimpleTrack:
go HandleRadio(m.commands, item)
return
case spotify.FullTrack:
go HandleRadio(m.commands, item.SimpleTrack)
return
case spotify.PlaylistTrack:
go HandleRadio(m.commands, item.Track.SimpleTrack)
return
case spotify.PlaylistItem:
go HandleRadio(m.commands, item.Track.Track.SimpleTrack)
return
case spotify.SavedTrack:
go HandleRadio(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, Queue:
m.mode = Main
new_items, err := MainView(m.commands)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case Album:
m.mode = Albums
new_items, err := AlbumsView(m.commands)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case Artist:
m.mode = Artists
new_items, err := ArtistsView(m.commands)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case ArtistAlbum:
m.mode = Artist
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case SearchArtists, SearchTracks, SearchAlbums, SearchPlaylists:
m.mode = Search
items, result, err := SearchView(m.commands, m.search)
if err != nil {
return nil, err
}
m.searchResults = result
m.list.SetItems(items)
case SearchArtist:
m.mode = SearchArtists
new_items, err := SearchArtistsView(m.commands, m.searchResults.Artists)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case SearchArtistAlbum:
m.mode = SearchArtist
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case SearchAlbum:
m.mode = SearchAlbums
new_items, err := SearchAlbumsView(m.commands, m.searchResults.Albums)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
case SearchPlaylist:
m.mode = SearchPlaylists
new_items, err := SearchPlaylistsView(m.commands, m.searchResults.Playlists)
if err != nil {
return nil, err
}
m.list.SetItems(new_items)
default:
page = 0
}
return nil, nil
}
type SpotifyUrl struct {
ExternalURLs map[string]string
}
func (m *mainModel) CopyToClipboard() error {
item := m.list.SelectedItem().(mainItem).SpotifyItem
switch converted := item.(type) {
case spotify.SimplePlaylist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullPlaylist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleAlbum:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullAlbum:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleArtist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullArtist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleTrack:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.PlaylistTrack:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.Track.ExternalURLs["spotify"])
case spotify.SavedTrack:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.FullTrack:
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.commands, item.Track.ID)
case spotify.SavedTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.commands, item.ID)
case spotify.SimpleTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.commands, item.ID)
case spotify.FullTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.commands, item.ID)
case *spotify.FullTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.commands, item.ID)
case *spotify.SimpleTrack:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.commands, item.ID)
case *spotify.SimplePlaylist:
go m.SendMessage("Adding "+item.Name+" to queue", 2*time.Second)
go HandleQueueItem(m.commands, item.ID)
}
if m.mode == Queue {
go func() {
new_items, err := QueueView(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.commands, track.ID, m.playlist.ID)
new_items, err := PlaylistView(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.commands, m.list.Index())
new_items, err := QueueView(m.commands)
if err != nil {
return
}
m.list.SetItems(new_items)
m.list.ResetSelected()
}()
case Search:
page = 1
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case *spotify.FullArtistPage:
m.mode = SearchArtists
new_items, err := SearchArtistsView(m.commands, item)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case *spotify.SimpleAlbumPage:
m.mode = SearchAlbums
new_items, err := SearchAlbumsView(m.commands, item)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case *spotify.SimplePlaylistPage:
m.mode = SearchPlaylists
new_items, err := SearchPlaylistsView(m.commands, item)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case *spotify.FullTrackPage:
m.mode = SearchTracks
new_items, err := SearchTracksView(item)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
}
case SearchArtists:
page = 1
m.mode = SearchArtist
m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist)
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil {
return err
}
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.album.ID, m.commands)
if err != nil {
return err
}
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.album.ID, m.commands)
if err != nil {
return err
}
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
new_items, err := PlaylistView(m.commands, playlist)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case Main:
page = 1
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case spotify.Queue:
m.mode = Queue
new_items, err := QueueView(m.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.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case *spotify.SavedAlbumPage:
m.mode = Albums
new_items, err := AlbumsView(m.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case spotify.SimplePlaylist:
m.mode = Playlist
m.playlist = item
new_items, err := PlaylistView(m.commands, item)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case *spotify.SavedTrackPage:
m.mode = Tracks
new_items, err := SavedTracksView(m.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
}
case Albums:
page = 1
m.mode = Album
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.album.ID, m.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case Artist:
m.mode = ArtistAlbum
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.album.ID, m.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case Artists:
m.mode = Artist
m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist)
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
m.list.ResetSelected()
case Album, ArtistAlbum, SearchArtistAlbum, SearchAlbum:
pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.TotalPages)
go HandlePlayWithContext(m.commands, &m.album.URI, &pos)
case Playlist, SearchPlaylist:
pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.PerPage)
go HandlePlayWithContext(m.commands, &m.playlist.URI, &pos)
case Tracks:
go HandlePlayLikedSong(m.commands, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage))
case SearchTracks:
go HandlePlayTrack(m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.FullTrack).ID)
case Devices:
go HandleSetDevice(m.commands, m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.PlayerDevice))
go m.SendMessage("Setting device to "+m.list.SelectedItem().FilterValue(), 2*time.Second)
m.mode = "main"
new_items, err := MainView(m.commands)
if err != nil {
return err
}
m.list.SetItems(new_items)
}
return nil
}
func (m *mainModel) Init() tea.Cmd {
main_updates = make(chan *mainModel)
return Tick()
}
type tickMsg time.Time
func Tick() tea.Cmd {
return tea.Tick(time.Second*1, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
func (m *mainModel) TickPlayback() {
playing, _ := m.commands.Client.PlayerCurrentlyPlaying(m.commands.Context)
if playing != nil && playing.Playing && playing.Item != nil {
currentlyPlaying = playing
playbackContext, _ = m.getContext(playing)
}
ticker := time.NewTicker(1 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
playing, _ := m.commands.Client.PlayerCurrentlyPlaying(m.commands.Context)
if playing != nil && playing.Playing && playing.Item != nil {
currentlyPlaying = playing
playbackContext, _ = m.getContext(playing)
}
case <-quit:
ticker.Stop()
return
}
}
}()
}
func (m *mainModel) View() string {
if m.input.Focused() {
return DocStyle.Render(m.list.View() + "\n" + m.input.View())
}
return DocStyle.Render(m.list.View() + "\n")
}
func (m *mainModel) Typing(msg tea.KeyMsg) (bool, tea.Cmd) {
if msg.String() == "enter" {
items, result, err := SearchView(m.commands, m.input.Value())
if err != nil {
return false, tea.Quit
}
m.searchResults = result
m.search = m.input.Value()
m.list.SetItems(items)
m.list.ResetSelected()
m.input.SetValue("")
m.input.Blur()
return true, nil
}
if msg.String() == "esc" {
m.input.SetValue("")
m.input.Blur()
return false, nil
}
m.input, _ = m.input.Update(msg)
return false, nil
}
func (m *mainModel) getContext(playing *spotify.CurrentlyPlaying) (string, error) {
context := playing.PlaybackContext
uri_split := strings.Split(string(context.URI), ":")
if len(uri_split) < 3 {
return "", fmt.Errorf("NO URI")
}
id := strings.Split(string(context.URI), ":")[2]
switch context.Type {
case "album":
album, err := m.commands.Client.GetAlbum(m.commands.Context, spotify.ID(id))
if err != nil {
return "", err
}
return album.Name, nil
case "playlist":
playlist, err := m.commands.Client.GetPlaylist(m.commands.Context, spotify.ID(id))
if err != nil {
return "", err
}
return playlist.Name, nil
case "artist":
artist, err := m.commands.Client.GetArtist(m.commands.Context, spotify.ID(id))
if err != nil {
return "", err
}
return artist.Name, nil
}
return "", nil
}
func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Update list items from LoadMore
select {
case update := <-main_updates:
m.list.SetItems(update.list.Items())
default:
}
// Call for more items if needed
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()
}
}
// Handle user input
switch msg := msg.(type) {
case tickMsg:
playing := currentlyPlaying
if playing != nil && playing.Playing && playing.Item != nil {
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.commands)
if err != nil {
return
}
m.list.SetItems(new_items)
}()
}
}
return m, tea.Batch(Tick(), cmd)
}
return m, Tick()
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
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
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
if msg.String() == "c" {
err := m.CopyToClipboard()
if err != nil {
return m, tea.Quit
}
}
if msg.String() == ">" {
go HandleSeek(m.commands, true)
}
if msg.String() == "<" {
go HandleSeek(m.commands, false)
}
if msg.String() == "+" {
go HandleVolume(m.commands, true)
}
if msg.String() == "-" {
go HandleVolume(m.commands, false)
}
// search input
if m.input.Focused() {
search, cmd := m.Typing(msg)
if search {
m.mode = "search"
}
return m, cmd
}
// start search
if msg.String() == "s" || msg.String() == "/" {
m.input.Focus()
}
// enter device selection
if msg.String() == "d" {
m.mode = Devices
new_items, err := DeviceView(m.commands)
if err != nil {
return m, tea.Quit
}
m.list.SetItems(new_items)
m.list.ResetSelected()
}
// go back
if msg.String() == "backspace" || msg.String() == "esc" || msg.String() == "q" {
msg, err := m.GoBack()
if err != nil {
return m, tea.Quit
}
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() == " " || msg.String() == "p" {
err := m.SelectItem()
if err != nil {
return m, tea.Quit
}
}
// start radio
if msg.String() == "ctrl+r" {
m.PlayRadio()
}
// handle mouse
case tea.MouseMsg:
if msg.Type == 5 {
m.list.CursorUp()
}
if msg.Type == 6 {
m.list.CursorDown()
}
// window size -1 to handle search bar
case tea.WindowSizeMsg:
h, v := DocStyle.GetFrameSize()
m.list.SetSize(msg.Width-h, msg.Height-v-1)
}
// return
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func InitMain(c *commands.Commander, mode Mode) (tea.Model, error) {
prog := progress.New(progress.WithColorProfile(2), progress.WithoutPercentage())
var err error
lipgloss.SetColorProfile(2)
items := []list.Item{}
switch mode {
case Main:
items, err = MainView(c)
if err != nil {
return nil, err
}
case Devices:
items, err = DeviceView(c)
if err != nil {
return nil, err
}
case Tracks:
items, err = SavedTracksView(c)
if err != nil {
return nil, err
}
}
m := &mainModel{
list: list.New(items, list.NewDefaultDelegate(), 0, 0),
commands: c,
mode: mode,
progress: prog,
}
m.list.Title = "GOSPT"
go m.TickPlayback()
Tick()
m.list.DisableQuitKeybindings()
m.list.SetFilteringEnabled(false)
m.list.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
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("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"+"p"), key.WithHelp("ctrl+p", "queue song")),
key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "select device")),
}
}
input := textinput.New()
input.Prompt = "$ "
input.Placeholder = "Search..."
input.CharLimit = 250
input.Width = 50
m.input = input
return m, nil
}

20
src/components/tui/tui.go Normal file
View File

@ -0,0 +1,20 @@
package tui
import (
tea "github.com/charmbracelet/bubbletea"
"git.asdf.cafe/abs3nt/gospt-ng/src/components/commands"
)
// StartTea the entry point for the UI. Initializes the model.
func StartTea(cmd *commands.Commander, mode string) error {
m, err := InitMain(cmd, Mode(mode))
if err != nil {
return err
}
P = tea.NewProgram(m, tea.WithAltScreen())
if _, err := P.Run(); err != nil {
return err
}
return nil
}

343
src/components/tui/views.go Normal file
View File

@ -0,0 +1,343 @@
package tui
import (
"fmt"
"regexp"
"time"
"github.com/charmbracelet/bubbles/list"
"github.com/zmb3/spotify/v2"
"golang.org/x/sync/errgroup"
"git.asdf.cafe/abs3nt/gospt-ng/src/components/commands"
)
const regex = `<.*?>`
func DeviceView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{}
devices, err := commands.Client.PlayerDevices(commands.Context)
if err != nil {
return nil, err
}
for _, device := range devices {
items = append(items, mainItem{
Name: device.Name,
Desc: fmt.Sprintf("%s - active: %t", device.ID, device.Active),
SpotifyItem: device,
})
}
return items, nil
}
func QueueView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{}
tracks, err := commands.Client.GetQueue(commands.Context)
if err != nil {
return nil, err
}
if tracks.CurrentlyPlaying.Name != "" {
items = append(items, mainItem{
Name: tracks.CurrentlyPlaying.Name,
Artist: tracks.CurrentlyPlaying.Artists[0],
Duration: tracks.CurrentlyPlaying.TimeDuration().Round(time.Second).String(),
ID: tracks.CurrentlyPlaying.ID,
Desc: tracks.CurrentlyPlaying.Artists[0].Name + " - " + tracks.CurrentlyPlaying.TimeDuration().
Round(time.Second).
String(),
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(commands *commands.Commander, playlist spotify.SimplePlaylist) ([]list.Item, error) {
items := []list.Item{}
playlistItems, err := commands.Client.GetPlaylistItems(
commands.Context,
playlist.ID,
spotify.Limit(50),
spotify.Offset(0),
)
if err != nil {
return nil, err
}
for _, item := range playlistItems.Items {
items = append(items, mainItem{
Name: item.Track.Track.Name,
Artist: item.Track.Track.Artists[0],
Duration: item.Track.Track.TimeDuration().Round(time.Second).String(),
ID: item.Track.Track.ID,
Desc: item.Track.Track.Artists[0].Name + " - " + item.Track.Track.TimeDuration().
Round(time.Second).
String(),
SpotifyItem: item,
})
}
return items, nil
}
func ArtistsView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{}
artists, err := commands.Client.CurrentUsersFollowedArtists(commands.Context, spotify.Limit(50), spotify.Offset(0))
if err != nil {
return nil, err
}
for _, artist := range artists.Artists {
items = append(items, mainItem{
Name: artist.Name,
ID: artist.ID,
Desc: fmt.Sprintf("%d followers", artist.Followers.Count),
SpotifyItem: artist.SimpleArtist,
})
}
return items, nil
}
func SearchArtistsView(
commands *commands.Commander,
artists *spotify.FullArtistPage,
) ([]list.Item, error) {
items := []list.Item{}
for _, artist := range artists.Artists {
items = append(items, mainItem{
Name: artist.Name,
ID: artist.ID,
Desc: fmt.Sprintf("%d followers", artist.Followers.Count),
SpotifyItem: artist.SimpleArtist,
})
}
return items, nil
}
func SearchView(commands *commands.Commander, search string) ([]list.Item, *SearchResults, error) {
items := []list.Item{}
result, err := commands.Search(search, 1)
if err != nil {
return nil, nil, err
}
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,
Albums: result.Albums,
Artists: result.Artists,
}
return items, results, nil
}
func AlbumsView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{}
albums, err := commands.Client.CurrentUsersAlbums(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return nil, err
}
for _, album := range albums.Albums {
items = append(items, mainItem{
Name: album.Name,
ID: album.ID,
Desc: fmt.Sprintf(
"%s by %s, %d tracks, released %d",
album.AlbumType,
album.Artists[0].Name,
album.Tracks.Total,
album.ReleaseDateTime().Year(),
),
SpotifyItem: album.SimpleAlbum,
})
}
return items, nil
}
func SearchPlaylistsView(commands *commands.Commander, playlists *spotify.SimplePlaylistPage) ([]list.Item, error) {
items := []list.Item{}
for _, playlist := range playlists.Playlists {
items = append(items, mainItem{
Name: playlist.Name,
Desc: stripHtmlRegex(playlist.Description),
SpotifyItem: playlist,
})
}
return items, nil
}
func SearchAlbumsView(commands *commands.Commander, albums *spotify.SimpleAlbumPage) ([]list.Item, error) {
items := []list.Item{}
for _, album := range albums.Albums {
items = append(items, mainItem{
Name: album.Name,
ID: album.ID,
Desc: fmt.Sprintf(
"%s by %s, released %d",
album.AlbumType,
album.Artists[0].Name,
album.ReleaseDateTime().Year(),
),
SpotifyItem: album,
})
}
return items, nil
}
func ArtistAlbumsView(album spotify.ID, commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{}
albums, err := commands.ArtistAlbums(album, 1)
if err != nil {
return nil, err
}
for _, album := range albums.Albums {
items = append(items, mainItem{
Name: album.Name,
ID: album.ID,
Desc: fmt.Sprintf("%s by %s", album.AlbumType, album.Artists[0].Name),
SpotifyItem: album,
})
}
return items, err
}
func AlbumTracksView(album spotify.ID, commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{}
tracks, err := commands.AlbumTracks(album, 1)
if err != nil {
return nil, err
}
for _, track := range tracks.Tracks {
items = append(items, mainItem{
Name: track.Name,
Artist: track.Artists[0],
Duration: track.TimeDuration().Round(time.Second).String(),
ID: track.ID,
SpotifyItem: track,
Desc: track.Artists[0].Name + " - " + track.TimeDuration().Round(time.Second).String(),
})
}
return items, err
}
func SearchTracksView(tracks *spotify.FullTrackPage) ([]list.Item, error) {
items := []list.Item{}
for _, track := range tracks.Tracks {
items = append(items, mainItem{
Name: track.Name,
Artist: track.Artists[0],
Duration: track.TimeDuration().Round(time.Second).String(),
ID: track.ID,
SpotifyItem: track,
Desc: track.Artists[0].Name + " - " + track.TimeDuration().Round(time.Second).String(),
})
}
return items, nil
}
func SavedTracksView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{}
tracks, err := commands.Client.CurrentUsersTracks(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil {
return nil, err
}
for _, track := range tracks.Tracks {
items = append(items, mainItem{
Name: track.Name,
Artist: track.Artists[0],
Duration: track.TimeDuration().Round(time.Second).String(),
ID: track.ID,
SpotifyItem: track,
Desc: track.Artists[0].Name + " - " + track.TimeDuration().Round(time.Second).String(),
})
}
return items, err
}
func MainView(c *commands.Commander) ([]list.Item, error) {
wg := errgroup.Group{}
var saved_items *spotify.SavedTrackPage
var playlists *spotify.SimplePlaylistPage
var artists *spotify.FullArtistCursorPage
var albums *spotify.SavedAlbumPage
wg.Go(func() (err error) {
saved_items, err = c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0))
return
})
wg.Go(func() (err error) {
playlists, err = c.Client.CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset(0))
return
})
wg.Go(func() (err error) {
artists, err = c.Client.CurrentUsersFollowedArtists(c.Context, spotify.Limit(50), spotify.Offset(0))
return
})
wg.Go(func() (err error) {
albums, err = c.Client.CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset(0))
return
})
err := wg.Wait()
if err != nil {
return nil, err
}
items := []list.Item{}
if saved_items != nil && saved_items.Total != 0 {
items = append(items, mainItem{
Name: "Saved Tracks",
Desc: fmt.Sprintf("%d saved songs", saved_items.Total),
SpotifyItem: saved_items,
})
}
if albums != nil && albums.Total != 0 {
items = append(items, mainItem{
Name: "Albums",
Desc: fmt.Sprintf("%d albums", albums.Total),
SpotifyItem: albums,
})
}
if artists != nil && artists.Total != 0 {
items = append(items, mainItem{
Name: "Artists",
Desc: fmt.Sprintf("%d artists", artists.Total),
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: stripHtmlRegex(playlist.Description),
SpotifyItem: playlist,
})
}
}
return items, nil
}
func stripHtmlRegex(s string) string {
r := regexp.MustCompile(regex)
return r.ReplaceAllString(s, "")
}