From fb16b7ac0e7f5ed1c5788884f3a128c53c8b50b7 Mon Sep 17 00:00:00 2001 From: StormFireFox1 Date: Sat, 13 Oct 2018 16:31:34 +0300 Subject: [PATCH] Add Spotify Web Widget --- main.go | 3 + spotifyweb/widget.go | 245 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 spotifyweb/widget.go diff --git a/main.go b/main.go index feb59f6c..6c1e01f6 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ import ( "github.com/senorprogrammer/wtf/power" "github.com/senorprogrammer/wtf/security" "github.com/senorprogrammer/wtf/spotify" + "github.com/senorprogrammer/wtf/spotifyweb" "github.com/senorprogrammer/wtf/status" "github.com/senorprogrammer/wtf/system" "github.com/senorprogrammer/wtf/textfile" @@ -225,6 +226,8 @@ func addWidget(app *tview.Application, pages *tview.Pages, widgetName string) { widgets = append(widgets, system.NewWidget(app, date, version)) case "spotify": widgets = append(widgets, spotify.NewWidget(app, pages)) + case "spotifyweb": + widgets = append(widgets, spotifyweb.NewWidget(app, pages)) case "textfile": widgets = append(widgets, textfile.NewWidget(app, pages)) case "todo": diff --git a/spotifyweb/widget.go b/spotifyweb/widget.go new file mode 100644 index 00000000..fb222a4d --- /dev/null +++ b/spotifyweb/widget.go @@ -0,0 +1,245 @@ +package spotifyweb + +import ( + "errors" + "fmt" + "net/http" + "os" + "time" + + "github.com/gdamore/tcell" + "github.com/rivo/tview" + "github.com/senorprogrammer/wtf/logger" + "github.com/senorprogrammer/wtf/wtf" + "github.com/zmb3/spotify" +) + +// HelpText contains the help text for the Spotify Web API widget. +const HelpText = ` +Keyboard commands for Spotify Web: + + Before any of these commands are used, you should authenticate using the + URL provided by the widget. + + The widget should automatically open a browser window for you, otherwise + you should check out the logs for the URL. + + /: Show/hide this help window + h: Switch to previous song in Spotify queue + l: Switch to next song in Spotify queue + s: Toggle shuffle + + [space]: Pause/play current song + + esc: Unselect the Spotify Web module +` + +// Info is the struct that contains all the information the Spotify player displays to the user +type Info struct { + Artists string + Title string + Album string + TrackNumber int + Status string +} + +// Widget is the struct used by all WTF widgets to transfer to the main widget controller +type Widget struct { + wtf.HelpfulWidget + wtf.TextWidget + Info + clientChan chan *spotify.Client + client *spotify.Client + playerState *spotify.PlayerState +} + +var ( + auth spotify.Authenticator + tempClientChan = make(chan *spotify.Client) + state = "wtfSpotifyWebStateString" + authURL string + callbackPort string + redirectURI string +) + +func authHandler(w http.ResponseWriter, r *http.Request) { + logger.Log("[SpotifyWeb] Got an authentication hit!") + tok, err := auth.Token(state, r) + if err != nil { + http.Error(w, "Couldn't get token", http.StatusForbidden) + logger.Log(err.Error()) + } + if st := r.FormValue("state"); st != state { + http.NotFound(w, r) + logger.Log(fmt.Sprintf("State mismatch: %s != %s\n", st, state)) + } + // use the token to get an authenticated client + client := auth.NewClient(tok) + fmt.Fprintf(w, "Login Completed!") + tempClientChan <- &client +} + +func clientID() string { + return wtf.Config.UString( + "wtf.mods.spotifyweb.clientID", + os.Getenv("SPOTIFY_ID"), + ) +} + +func secretKey() string { + return wtf.Config.UString( + "wtf.mods.spotifyweb.secretKey", + os.Getenv("SPOTIFY_SECRET"), + ) +} + +// NewWidget creates a new widget for WTF +func NewWidget(app *tview.Application, pages *tview.Pages) *Widget { + callbackPort = wtf.Config.UString("wtf.mods.spotifyweb.callbackPort", "8080") + redirectURI = "http://localhost:" + callbackPort + "/callback" + + auth = spotify.NewAuthenticator(redirectURI, spotify.ScopeUserReadCurrentlyPlaying, spotify.ScopeUserReadPlaybackState, spotify.ScopeUserModifyPlaybackState) + auth.SetAuthInfo(clientID(), secretKey()) + authURL = auth.AuthURL(state) + + var client *spotify.Client + var playerState *spotify.PlayerState + + widget := Widget{ + HelpfulWidget: wtf.NewHelpfulWidget(app, pages, HelpText), + TextWidget: wtf.NewTextWidget(app, "SpotifyWeb", "spotifyweb", true), + Info: Info{}, + clientChan: tempClientChan, + client: client, + playerState: playerState, + } + + http.HandleFunc("/callback", authHandler) + go http.ListenAndServe(":"+callbackPort, nil) + + go func() { + // wait for auth to complete + logger.Log("[SpotifyWeb] Waiting for authentication... URL: " + authURL) + client = <-tempClientChan + + // use the client to make calls that require authorization + _, err := client.CurrentUser() + if err != nil { + panic(err) + } + + playerState, err = client.PlayerState() + if err != nil { + panic(err) + } + logger.Log("[SpotifyWeb] Authentication complete.") + widget.client = client + widget.playerState = playerState + widget.Refresh() + }() + + // While I wish I could find the reason this doesn't work, I can't. + // + // Normally, this should open the URL to the browser, however it opens the Explorer window in Windows. + // This mostly likely has to do with the fact that the URL includes some very special characters that no terminal likes. + // The only solution would be to include quotes in the command, which is why I do here, but it doesn't work. + // + // If inconvenient, I'll remove this option and save the URL in a file or some other method. + wtf.OpenFile(`"` + authURL + `"`) + + widget.HelpfulWidget.SetView(widget.View) + widget.TextWidget.RefreshInt = 5 + widget.View.SetInputCapture(widget.captureInput) + widget.View.SetWrap(true) + widget.View.SetWordWrap(true) + widget.View.SetTitle("[green]Spotify Web[white]") + return &widget +} + +func (w *Widget) refreshSpotifyInfos() error { + if w.client == nil || w.playerState == nil { + return errors.New("Authentication failed! Please log in to Spotify by visiting the following page in your browser: " + authURL) + } + var err error + w.playerState, err = w.client.PlayerState() + if err != nil { + return errors.New("Extracting player state failed! Please refresh or restart WTF") + } + w.Info.Album = fmt.Sprint(w.playerState.CurrentlyPlaying.Item.Album.Name) + artists := "" + for _, artist := range w.playerState.CurrentlyPlaying.Item.Artists { + artists += artist.Name + ", " + } + artists = artists[:len(artists)-2] + w.Info.Artists = artists + w.Info.Title = fmt.Sprint(w.playerState.CurrentlyPlaying.Item.Name) + w.Info.TrackNumber = w.playerState.CurrentlyPlaying.Item.TrackNumber + if w.playerState.CurrentlyPlaying.Playing { + w.Info.Status = "Playing" + } else { + w.Info.Status = "Paused" + } + return nil +} + +// Refresh refreshes the current view of the widget +func (w *Widget) Refresh() { + w.render() +} + +func (w *Widget) render() { + err := w.refreshSpotifyInfos() + w.View.Clear() + if err != nil { + w.TextWidget.View.SetText(err.Error()) + } else { + w.TextWidget.View.SetText(w.createOutput()) + } +} + +func (w *Widget) captureInput(event *tcell.EventKey) *tcell.EventKey { + switch (string)(event.Rune()) { + case "/": + w.ShowHelp() + return nil + case "h": + w.client.Previous() + time.Sleep(time.Millisecond * 500) + w.Refresh() + return nil + case "l": + w.client.Next() + time.Sleep(time.Millisecond * 500) + w.Refresh() + return nil + case " ": + if w.playerState.CurrentlyPlaying.Playing { + w.client.Pause() + } else { + w.client.Play() + } + time.Sleep(time.Millisecond * 500) + w.Refresh() + return nil + case "s": + w.playerState.ShuffleState = !w.playerState.ShuffleState + w.client.Shuffle(w.playerState.ShuffleState) + time.Sleep(time.Millisecond * 500) + w.Refresh() + return nil + } + return nil +} + +func (w *Widget) createOutput() string { + output := wtf.CenterText(fmt.Sprintf("[green]Now %v [white]\n", w.Info.Status), w.Width()) + output += wtf.CenterText(fmt.Sprintf("[green]Title:[white] %v\n", w.Info.Title), w.Width()) + output += wtf.CenterText(fmt.Sprintf("[green]Artist:[white] %v\n", w.Info.Artists), w.Width()) + output += wtf.CenterText(fmt.Sprintf("[green]Album:[white] %v\n", w.Info.Album), w.Width()) + if w.playerState.ShuffleState { + output += wtf.CenterText(fmt.Sprintf("[green]Shuffle:[white] on\n"), w.Width()) + } else { + output += wtf.CenterText(fmt.Sprintf("[green]Shuffle:[white] off\n"), w.Width()) + } + return output +}