add experimental youtube-link command. fix pagination issues throughout tui

This commit is contained in:
abs3nt 2023-04-14 19:28:31 -07:00
parent ee9c2299dc
commit 8f179f0bc6
Signed by: abs3nt
GPG Key ID: FDC6662313FA9386
6 changed files with 187 additions and 2 deletions

9
go.mod
View File

@ -12,19 +12,24 @@ require (
github.com/cristalhq/aconfig/aconfigyaml v0.17.1
github.com/spf13/cobra v1.6.1
github.com/zmb3/spotify/v2 v2.3.1
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5
google.golang.org/api v0.30.0
modernc.org/sqlite v1.20.4
tuxpa.in/a/zlog v1.60.0
)
require (
cloud.google.com/go v0.65.0 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
@ -41,14 +46,16 @@ require (
github.com/rs/zerolog v1.28.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.opencensus.io v0.22.4 // indirect
golang.org/x/mod v0.3.0 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
google.golang.org/grpc v1.31.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect

7
go.sum
View File

@ -12,6 +12,7 @@ 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=
@ -82,6 +83,7 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -131,6 +133,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
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/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/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=
@ -212,6 +215,7 @@ 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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -408,6 +412,7 @@ 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -445,6 +450,7 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY
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/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@ -457,6 +463,7 @@ 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/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=

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

@ -0,0 +1,28 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// youtubeLinkCmd represents the youtube-link command
var youtubeLinkCmd = &cobra.Command{
Use: "youtube-link",
Aliases: []string{"yl"},
Short: "Print youtube link to currently playing song",
Long: `Print youtube link to currently playing song`,
Run: func(cmd *cobra.Command, args []string) {
link, err := commands.YoutubeLink(ctx)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Print(link)
},
}
func init() {
rootCmd.AddCommand(youtubeLinkCmd)
}

View File

@ -18,6 +18,7 @@ import (
"git.asdf.cafe/abs3nt/gospt/src/auth"
"git.asdf.cafe/abs3nt/gospt/src/cache"
"git.asdf.cafe/abs3nt/gospt/src/gctx"
"git.asdf.cafe/abs3nt/gospt/src/youtube"
"github.com/zmb3/spotify/v2"
_ "modernc.org/sqlite"
@ -913,6 +914,15 @@ func (c *Commands) Link(ctx *gctx.Context) (string, error) {
return state.Item.ExternalURLs["spotify"], nil
}
func (c *Commands) YoutubeLink(ctx *gctx.Context) (string, error) {
state, err := c.Client().PlayerState(ctx)
if err != nil {
return "", err
}
link := youtube.Search(state.Item.Artists[0].Name + state.Item.Name)
return link, nil
}
func (c *Commands) LinkContext(ctx *gctx.Context) (string, error) {
state, err := c.Client().PlayerState(ctx)
if err != nil {

View File

@ -125,6 +125,7 @@ func (m *mainModel) PlayRadio() {
}
func (m *mainModel) GoBack() (tea.Cmd, error) {
page = 1
switch m.mode {
case Main:
return tea.Quit, nil
@ -242,6 +243,7 @@ func (m *mainModel) CopyToClipboard() error {
func (m *mainModel) SelectItem() error {
switch m.mode {
case Search:
page = 1
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case *spotify.FullArtistPage:
m.mode = SearchArtists
@ -277,6 +279,7 @@ func (m *mainModel) SelectItem() error {
m.list.ResetSelected()
}
case SearchArtists:
page = 1
m.mode = SearchArtist
m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist)
new_items, err := ArtistAlbumsView(m.ctx, m.artist.ID, m.commands)
@ -286,6 +289,7 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
m.list.ResetSelected()
case SearchArtist:
page = 1
m.mode = SearchArtistAlbum
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.ctx, m.album.ID, m.commands)
@ -295,6 +299,7 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
m.list.ResetSelected()
case SearchAlbums:
page = 1
m.mode = SearchAlbum
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.ctx, m.album.ID, m.commands)
@ -304,6 +309,7 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
m.list.ResetSelected()
case SearchPlaylists:
page = 1
m.mode = SearchPlaylist
playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist)
m.playlist = playlist
@ -314,6 +320,7 @@ func (m *mainModel) SelectItem() error {
m.list.SetItems(new_items)
m.list.ResetSelected()
case Main:
page = 1
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case *spotify.FullArtistCursorPage:
m.mode = Artists
@ -350,6 +357,7 @@ func (m *mainModel) SelectItem() error {
m.list.ResetSelected()
}
case Albums:
page = 1
m.mode = Album
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.ctx, m.album.ID, m.commands)
@ -504,7 +512,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
default:
}
// Call for more items if needed
if m.list.Paginator.Page == m.list.Paginator.TotalPages-2 && m.list.Cursor() == 0 {
if m.list.Paginator.Page == m.list.Paginator.TotalPages-1 && m.list.Cursor() == 0 {
// if last request was still full request more
if len(m.list.Items())%50 == 0 {
go m.LoadMoreItems()

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

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