From 3b8a51db39b142195fa267782ec29eaa98e745b0 Mon Sep 17 00:00:00 2001 From: abs3nt Date: Sun, 18 Feb 2024 15:04:34 -0800 Subject: [PATCH] tui exists again --- go.mod | 48 +- go.sum | 143 +++- src/components/cli/cli.go | 8 + src/components/commands/activate_device.go | 5 +- src/components/commands/album.go | 18 + src/components/commands/artist.go | 12 + src/components/commands/next.go | 2 +- src/components/commands/play.go | 104 ++- src/components/commands/playlist.go | 23 + src/components/commands/previous.go | 2 +- src/components/commands/queue.go | 25 + src/components/commands/radio.go | 265 ++++++- src/components/commands/search.go | 12 + src/components/tui/handlers.go | 120 +++ src/components/tui/loader.go | 165 +++++ src/components/tui/main.go | 810 +++++++++++++++++++++ src/components/tui/tui.go | 20 + src/components/tui/views.go | 343 +++++++++ 18 files changed, 2092 insertions(+), 33 deletions(-) create mode 100644 src/components/commands/album.go create mode 100644 src/components/commands/artist.go create mode 100644 src/components/commands/playlist.go create mode 100644 src/components/commands/queue.go create mode 100644 src/components/commands/search.go create mode 100644 src/components/tui/handlers.go create mode 100644 src/components/tui/loader.go create mode 100644 src/components/tui/main.go create mode 100644 src/components/tui/tui.go create mode 100644 src/components/tui/views.go diff --git a/go.mod b/go.mod index 63c9222..afc5766 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index c4aa274..45ed817 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/src/components/cli/cli.go b/src/components/cli/cli.go index cb1fb22..f324f76 100644 --- a/src/components/cli/cli.go +++ b/src/components/cli/cli.go @@ -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", diff --git a/src/components/commands/activate_device.go b/src/components/commands/activate_device.go index 6a987d7..614d1c4 100644 --- a/src/components/commands/activate_device.go +++ b/src/components/commands/activate_device.go @@ -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 } diff --git a/src/components/commands/album.go b/src/components/commands/album.go new file mode 100644 index 0000000..6c4c3ff --- /dev/null +++ b/src/components/commands/album.go @@ -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)) +} diff --git a/src/components/commands/artist.go b/src/components/commands/artist.go new file mode 100644 index 0000000..30a4c99 --- /dev/null +++ b/src/components/commands/artist.go @@ -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 +} diff --git a/src/components/commands/next.go b/src/components/commands/next.go index 7e3f763..88342ca 100644 --- a/src/components/commands/next.go +++ b/src/components/commands/next.go @@ -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 } diff --git a/src/components/commands/play.go b/src/components/commands/play.go index ce4174d..74b9658 100644 --- a/src/components/commands/play.go +++ b/src/components/commands/play.go @@ -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 +} diff --git a/src/components/commands/playlist.go b/src/components/commands/playlist.go new file mode 100644 index 0000000..bd8d7a4 --- /dev/null +++ b/src/components/commands/playlist.go @@ -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)) +} diff --git a/src/components/commands/previous.go b/src/components/commands/previous.go index e12934b..9169741 100644 --- a/src/components/commands/previous.go +++ b/src/components/commands/previous.go @@ -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 } diff --git a/src/components/commands/queue.go b/src/components/commands/queue.go new file mode 100644 index 0000000..2981d08 --- /dev/null +++ b/src/components/commands/queue.go @@ -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 +} diff --git a/src/components/commands/radio.go b/src/components/commands/radio.go index 7a4a1f5..784060e 100644 --- a/src/components/commands/radio.go +++ b/src/components/commands/radio.go @@ -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 +} diff --git a/src/components/commands/search.go b/src/components/commands/search.go new file mode 100644 index 0000000..1d62862 --- /dev/null +++ b/src/components/commands/search.go @@ -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 +} diff --git a/src/components/tui/handlers.go b/src/components/tui/handlers.go new file mode 100644 index 0000000..0e79df3 --- /dev/null +++ b/src/components/tui/handlers.go @@ -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 + } +} diff --git a/src/components/tui/loader.go b/src/components/tui/loader.go new file mode 100644 index 0000000..77dd465 --- /dev/null +++ b/src/components/tui/loader.go @@ -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 + } +} diff --git a/src/components/tui/main.go b/src/components/tui/main.go new file mode 100644 index 0000000..6849caf --- /dev/null +++ b/src/components/tui/main.go @@ -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 +} diff --git a/src/components/tui/tui.go b/src/components/tui/tui.go new file mode 100644 index 0000000..ca34999 --- /dev/null +++ b/src/components/tui/tui.go @@ -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 +} diff --git a/src/components/tui/views.go b/src/components/tui/views.go new file mode 100644 index 0000000..a4172e4 --- /dev/null +++ b/src/components/tui/views.go @@ -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, "") +}