diff --git a/.gitignore b/.gitignore index e43b0f9..7200a57 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +.env +likes* diff --git a/README.md b/README.md index c410dda..4791f63 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,14 @@ export SPOTIFY_SECRET=xxx ## Usage +### login + +To login, add `"http://localhost:8080/callback"` to your spotify app redirect URLs + ### Commands + List of available commands: + ``` $ ./spotifycli --help A command line interface to manage Spotify playlists. @@ -47,6 +53,7 @@ Use "spotifycli [command] --help" for more information about a command. ``` ### Search + Search using query terms on top of tracks (`tr`), albums (`al`), artists (`ar`) or playlists (`pl`) by name. ``` @@ -63,6 +70,7 @@ Flags: ``` Sample search for type `tr` (track). + ``` ./spotifycli search --t "tr" --q "one step closer - live" ``` diff --git a/cmd/auth.go b/cmd/auth.go index adf50d5..62bb507 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -51,7 +51,12 @@ func authorize(cmd *cobra.Command, args []string) error { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Println("Got request for: ", r.URL.String()) }) - go http.ListenAndServe(":8080", nil) + go func() { + err := http.ListenAndServe(":8080", nil) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } + }() // User authentication process fmt.Println("authorize") @@ -67,7 +72,7 @@ func (handler *authenticationHandler) ServeHTTP(w http.ResponseWriter, r *http.R token, err := handler.auth.Token(handler.state, r) if err != nil { http.Error(w, "Couldn't get token", http.StatusForbidden) - log.Fatal(err) + log.Fatal("handler.auth.Token: ", err) } if st := r.FormValue("state"); st != handler.state { http.NotFound(w, r) diff --git a/cmd/playlist.go b/cmd/playlist.go index cab8964..f6cee03 100644 --- a/cmd/playlist.go +++ b/cmd/playlist.go @@ -12,38 +12,26 @@ import ( ) var ( - addtoPlaylistName string -) + addToPlaylistName string -var ( trackID string -) -var ( addTrackID string addTrackByIDToPlaylistName string -) -var ( addTrackName string addTrackByNameToPlaylistName string -) -var ( rmTrackName string rmTrackFromPlaylistName string -) -var ( newPlaylistName string -) -var ( delPlaylistName string -) -var ( - listPlaylistTracksName string + flagListPlaylistTracksName string + + spotifyMaxLimit = 100 ) func newCurrentTrackCmd() *cobra.Command { @@ -58,27 +46,29 @@ func newCurrentTrackCmd() *cobra.Command { } func newShowTrackCmd() *cobra.Command { - addtoCmd := &cobra.Command{ + addToCmd := &cobra.Command{ Use: "show --tid [TRACK_ID]", Short: "Display information about a track by ID", RunE: func(cmd *cobra.Command, args []string) error { return displayTrackById(cmd, args) }, } - addtoCmd.Flags().StringVar(&trackID, "tid", "", "Id of track to display.") - return addtoCmd + addToCmd.Flags().StringVar(&trackID, "tid", "", "Id of track to display.") + + return addToCmd } -func newAddtoPlaylistCmd() *cobra.Command { - addtoCmd := &cobra.Command{ +func newAddToPlaylistCmd() *cobra.Command { + addToCmd := &cobra.Command{ Use: "ato --p [PLAYLIST_NAME]", Short: "Add currently playing track to playlist", RunE: func(cmd *cobra.Command, args []string) error { - return addto(cmd, args) + return addTo(cmd, args) }, } - addtoCmd.Flags().StringVar(&addtoPlaylistName, "p", "", "Add current track to specified playlist.") - return addtoCmd + addToCmd.Flags().StringVar(&addToPlaylistName, "p", "", "Add current track to specified playlist.") + + return addToCmd } func newAddTrackByIDToPlaylistCmd() *cobra.Command { @@ -163,7 +153,9 @@ func newListPlaylistTracksCmd() *cobra.Command { return listTracksFromPlaylist(cmd, args) }, } - listCmd.Flags().StringVar(&listPlaylistTracksName, "p", "", "Name of playlist to list tracks from.") + + listCmd.Flags().StringVar(&flagListPlaylistTracksName, "p", "", "Name of playlist to list tracks from.") + return listCmd } @@ -190,51 +182,41 @@ func displayTrack(track *spotify.FullTrack) error { } func displayTrackById(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() + + // get the track (check for existence) + track, err := client.GetTrack(spotify.ID(trackID)) if err != nil { return err } - fmt.Println("User: ", user.DisplayName) - // get the track (check for existence) - track, err := client.GetTrack(spotify.ID(trackID)) + err = displayTrack(track) if err != nil { return err } - displayTrack(track) - return nil + return nil } func displayCurrentTrack(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() + + // get current playing song + playing, err := client.PlayerCurrentlyPlaying() if err != nil { return err } - fmt.Println("User: ", user.DisplayName) - // get current playing song - playing, err := client.PlayerCurrentlyPlaying() + err = displayTrack(playing.Item) if err != nil { return err } - displayTrack(playing.Item) return nil } -func addto(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() - if err != nil { - return err - } - fmt.Println("User: ", user.DisplayName) +func addTo(cmd *cobra.Command, args []string) error { // get my playlists - pl, err := getPlaylistByName(addtoPlaylistName) + pl, err := getPlaylistByName(addToPlaylistName) if err != nil { return err } @@ -248,7 +230,7 @@ func addto(cmd *cobra.Command, args []string) error { fmt.Println("Track: ", playing.Item.Name) // add track to playlist - _, err = client.AddTracksToPlaylist(user.ID, pl.ID, playing.Item.ID) + _, err = client.AddTracksToPlaylist(pl.ID, playing.Item.ID) if err != nil { return err } @@ -257,12 +239,6 @@ func addto(cmd *cobra.Command, args []string) error { } func listPlaylists(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() - if err != nil { - return err - } - fmt.Println("User: ", user.DisplayName) // get all playlists for the user playlists, err := getPlaylists() @@ -295,15 +271,13 @@ func listPlaylists(cmd *cobra.Command, args []string) error { } func newPlaylist(cmd *cobra.Command, args []string) error { - // current user user, err := client.CurrentUser() if err != nil { return err } - fmt.Println("User: ", user.DisplayName) // create new playlist - playlist, err := client.CreatePlaylistForUser(user.ID, newPlaylistName, true) + playlist, err := client.CreatePlaylistForUser(user.ID, newPlaylistName, "", true) if err != nil { return err } @@ -312,12 +286,10 @@ func newPlaylist(cmd *cobra.Command, args []string) error { } func deletePlaylist(cmd *cobra.Command, args []string) error { - // current user user, err := client.CurrentUser() if err != nil { return err } - fmt.Println("User: ", user.DisplayName) // get the playlist pl, err := getPlaylistByName(delPlaylistName) @@ -331,12 +303,6 @@ func deletePlaylist(cmd *cobra.Command, args []string) error { } func addTrackByIDToPlaylist(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() - if err != nil { - return err - } - fmt.Println("User: ", user.DisplayName) // get the playlist by name pl, err := getPlaylistByName(addTrackByIDToPlaylistName) @@ -353,7 +319,7 @@ func addTrackByIDToPlaylist(cmd *cobra.Command, args []string) error { fmt.Println("Track: ", tr.Name) // add track to playlist - _, err = client.AddTracksToPlaylist(user.ID, pl.ID, tr.ID) + _, err = client.AddTracksToPlaylist(pl.ID, tr.ID) if err != nil { return err } @@ -362,12 +328,6 @@ func addTrackByIDToPlaylist(cmd *cobra.Command, args []string) error { } func addTrackByNameToPlaylist(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() - if err != nil { - return err - } - fmt.Println("User: ", user.DisplayName) // get the playlist by name pl, err := getPlaylistByName(addTrackByNameToPlaylistName) @@ -389,7 +349,7 @@ func addTrackByNameToPlaylist(cmd *cobra.Command, args []string) error { fmt.Println("Track: ", tracks[0].Name) // add track to playlist - _, err = client.AddTracksToPlaylist(user.ID, pl.ID, tracks[0].ID) + _, err = client.AddTracksToPlaylist(pl.ID, tracks[0].ID) if err != nil { return err } @@ -401,12 +361,6 @@ func addTrackByNameToPlaylist(cmd *cobra.Command, args []string) error { } func rmTrackByNameFromPlaylist(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() - if err != nil { - return err - } - fmt.Println("User: ", user.DisplayName) // get the playlist by name pl, err := getPlaylistByName(rmTrackFromPlaylistName) @@ -416,7 +370,10 @@ func rmTrackByNameFromPlaylist(cmd *cobra.Command, args []string) error { // get track in playlist and validate existence var matchedTrack spotify.SimpleTrack - ptracks, err := client.GetPlaylistTracks(user.ID, pl.ID) + ptracks, err := client.GetPlaylistTracks(pl.ID) + if err != nil { + return err + } for _, t := range ptracks.Tracks { if rmTrackName == t.Track.SimpleTrack.Name { matchedTrack = t.Track.SimpleTrack @@ -429,7 +386,7 @@ func rmTrackByNameFromPlaylist(cmd *cobra.Command, args []string) error { fmt.Println("Track: ", matchedTrack.Name) // remove track from playlist - _, err = client.RemoveTracksFromPlaylist(user.ID, pl.ID, matchedTrack.ID) + _, err = client.RemoveTracksFromPlaylist(pl.ID, matchedTrack.ID) if err != nil { return err } @@ -438,44 +395,69 @@ func rmTrackByNameFromPlaylist(cmd *cobra.Command, args []string) error { } func listTracksFromPlaylist(cmd *cobra.Command, args []string) error { - // current user - user, err := client.CurrentUser() + pl, err := getPlaylistByName(flagListPlaylistTracksName) if err != nil { return err } - fmt.Println("User: ", user.DisplayName) - pl, err := getPlaylistByName(listPlaylistTracksName) - if err != nil { - return err + opts := &spotify.Options{ + Limit: &spotifyMaxLimit, } - // get tracks from playlist - tracks, err := client.GetPlaylistTracks(user.ID, pl.ID) - if err != nil { - return err + var tracks []spotify.PlaylistTrack + i := 0 + + for { + offset := i + opts.Offset = &offset + + res, err := client.GetPlaylistTracksOpt(pl.ID, opts, "") + if err != nil { + return fmt.Errorf("could not get playlist tracks: %v", err) + } + + tracks = append(tracks, res.Tracks...) + + if len(res.Tracks) < spotifyMaxLimit { + break + } + + i += spotifyMaxLimit } // format resulting data - var data [][]interface{} - if tracks.Tracks != nil { - for _, item := range tracks.Tracks { + var data = make([][]interface{}, 0, len(tracks)) + + if len(tracks) > 0 { + for _, item := range tracks { + artist := item.Track.Artists[0].Name + + if len(item.Track.Artists) > 0 { + for _, a := range item.Track.Artists { + artist += ", " + a.Name + } + } + track := []string{ - string(item.Track.ID), - item.Track.Name, - item.Track.Album.Name, item.Track.Artists[0].Name, - strconv.Itoa(item.Track.Popularity)} + item.Track.Name, + secondsToMinutes(item.Track.Duration / 1000), + string(item.Track.ID), + } + row := make([]interface{}, len(track)) + for i, d := range track { row[i] = d } + data = append(data, row) } + + // pretty print track results + printSimple([]string{"Artist", "Name", "Duration", "ID"}, data) } - // pretty print track results - printSimple([]string{"ID", "Name", "Album", "Artist", "Popularity"}, data) return nil } @@ -485,7 +467,7 @@ func getPlaylists() (*spotify.SimplePlaylistPage, error) { return &(spotify.SimplePlaylistPage{}), err } - return playlists, nil + return playlists, nil } func getPlaylistByName(playlistName string) (spotify.SimplePlaylist, error) { @@ -510,3 +492,14 @@ func getPlaylistByName(playlistName string) (spotify.SimplePlaylist, error) { } return matchPlaylist, nil } + +func secondsToMinutes(seconds int) string { + minutes := seconds / 60 + remainder := seconds % 60 + + if remainder < 10 { + return fmt.Sprintf("%d:0%d", minutes, remainder) + } + + return fmt.Sprintf("%d:%d", minutes, remainder) +} diff --git a/cmd/root.go b/cmd/root.go index a8f4d73..3212d23 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,8 +28,8 @@ func NewRootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "spotifycli", Short: "A command line interface to manage Spotify playlists.", - PersistentPreRun: prerun, - PersistentPostRun: postrun, + PersistentPreRun: preRun, + PersistentPostRun: postRun, } // auth ops rootCmd.AddCommand(newLoginCmd()) @@ -43,16 +43,17 @@ func NewRootCmd() *cobra.Command { rootCmd.AddCommand(newListPlaylistsCmd()) rootCmd.AddCommand(newCreatePlaylistCmd()) rootCmd.AddCommand(newDeletePlaylistCmd()) - rootCmd.AddCommand(newAddtoPlaylistCmd()) + rootCmd.AddCommand(newAddToPlaylistCmd()) rootCmd.AddCommand(newAddTrackByIDToPlaylistCmd()) rootCmd.AddCommand(newAddTrackByNameToPlaylistCmd()) rootCmd.AddCommand(newRemoveTrackFromPlaylistCmd()) rootCmd.AddCommand(newListPlaylistTracksCmd()) rootCmd.AddCommand(newShowTrackCmd()) + return rootCmd } -func prerun(cmd *cobra.Command, args []string) { +func preRun(cmd *cobra.Command, args []string) { // initialize authenticator auth = spotify.NewAuthenticator( redirectURI, @@ -72,13 +73,13 @@ func prerun(cmd *cobra.Command, args []string) { token, err := getToken() if err != nil { if err := authorize(cmd, args); err != nil { - log.Fatal(err) + log.Fatal("authorize: ", err) } } client = auth.NewClient(token) } -func postrun(cmd *cobra.Command, args []string) { +func postRun(cmd *cobra.Command, args []string) { // exit early if cmd.Use == "login" || cmd.Use == "logout" { return @@ -87,15 +88,15 @@ func postrun(cmd *cobra.Command, args []string) { // refresh token currTok, err := client.Token() if err != nil { - log.Fatal(err) + log.Fatal("client token: ", err) } token, err := getToken() if err != nil { - log.Fatal(err) + log.Fatal("getToken: ", err) } if token != currTok { if err := persistToken(token); err != nil { - log.Fatal(err) + log.Fatal("persistToken: ", err) } } } diff --git a/cmd/search.go b/cmd/search.go index 7694e49..aa59274 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -40,7 +40,7 @@ func search(cmd *cobra.Command, args []string) error { case "pl": return displaySearchPlaylists(searchQuery) default: - return errors.New("Not supported") + return errors.New("not supported") } } @@ -194,6 +194,11 @@ func searchPlaylists(query string) ([][]interface{}, error) { func printSimple(headers []string, data [][]interface{}) { tabulate := gotabulate.Create(data) + tabulate.SetHeaders(headers) + tabulate.SetAlign("left") + // tabulate.SetMaxCellSize(50) + // tabulate.SetWrapStrings(true) + fmt.Println(tabulate.Render("simple")) } diff --git a/go.mod b/go.mod index 6e3dc05..55217ab 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.15 require ( github.com/agext/uuid v1.0.1 github.com/bndr/gotabulate v1.1.2 - github.com/golang/protobuf v1.1.0 + github.com/golang/protobuf v1.2.0 github.com/inconshreveable/mousetrap v1.0.0 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.1 - github.com/zmb3/spotify v0.0.0-20180212041948-79deba8533f6 - golang.org/x/net v0.0.0-20180530234432-1e491301e022 - golang.org/x/oauth2 v0.0.0-20180529203656-ec22f46f877b - google.golang.org/appengine v1.0.0 + github.com/zmb3/spotify v1.3.0 + golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + google.golang.org/appengine v1.4.0 ) diff --git a/go.sum b/go.sum index cd3978b..02f1671 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,37 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/agext/uuid v1.0.1 h1:Za6toqrdwQwHJzTfsgt3JaPuQ2xeYuak6N0513NdFeQ= github.com/agext/uuid v1.0.1/go.mod h1:S35kGCagARSdmRVLoWmwmevvhFsiZkmtEqdw4hcDePs= github.com/bndr/gotabulate v1.1.2 h1:yC9izuZEphojb9r+KYL4W9IJKO/ceIO8HDwxMA24U4c= github.com/bndr/gotabulate v1.1.2/go.mod h1:0+8yUgaPTtLRTjf49E8oju7ojpU11YmXyvq1LbPAb3U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/zmb3/spotify v0.0.0-20180212041948-79deba8533f6 h1:07d/UYzbd/YHT31y8aiRg/eG7YuTqYm1XPgXMg2PEAo= github.com/zmb3/spotify v0.0.0-20180212041948-79deba8533f6/go.mod h1:pHsWAmY9PfX7i/uwPZkmWrebc8JbK8FppKbvyevwzSU= +github.com/zmb3/spotify v1.3.0 h1:6Z2F1IMx0Hviq/dpf8nFwvKPppFEMXn8yfReSBVi16k= +github.com/zmb3/spotify v1.3.0/go.mod h1:GD7AAEMUJVYc2Z7p2a2S0E3/5f/KxM/vOnErNr4j+Tw= golang.org/x/net v0.0.0-20180530234432-1e491301e022 h1:MVYFTUmVD3/+ERcvRRI+P/C2+WOUimXh+Pd8LVsklZ4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180529203656-ec22f46f877b h1:nCwwlzLoBQhkY/S3CJ2CGAU4pYfR8+5/TPGEHT+p5Nk= golang.org/x/oauth2 v0.0.0-20180529203656-ec22f46f877b/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index da776f2..04979bd 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,12 @@ package main import ( "log" - "os" "github.com/masroorhasan/spotifycli/cmd" ) func main() { if err := cmd.NewRootCmd().Execute(); err != nil { - log.Fatalln(err) - os.Exit(1) + log.Fatal("execute: ", err) } }