github.com/wtfutil/wtf@v0.43.0/modules/spotifyweb/widget.go (about)

     1  package spotifyweb
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  	"time"
     9  
    10  	"github.com/rivo/tview"
    11  	"github.com/wtfutil/wtf/utils"
    12  	"github.com/wtfutil/wtf/view"
    13  	"github.com/zmb3/spotify"
    14  )
    15  
    16  var (
    17  	auth           spotify.Authenticator
    18  	tempClientChan = make(chan *spotify.Client)
    19  	state          = "wtfSpotifyWebStateString"
    20  	authURL        string
    21  	callbackPort   string
    22  	redirectURI    string
    23  )
    24  
    25  // Info is the struct that contains all the information the Spotify player displays to the user
    26  type Info struct {
    27  	Artists     string
    28  	Title       string
    29  	Album       string
    30  	TrackNumber int
    31  	Status      string
    32  }
    33  
    34  // Widget is the struct used by all WTF widgets to transfer to the main widget controller
    35  type Widget struct {
    36  	view.TextWidget
    37  
    38  	Info
    39  
    40  	client      *spotify.Client
    41  	clientChan  chan *spotify.Client
    42  	playerState *spotify.PlayerState
    43  	settings    *Settings
    44  }
    45  
    46  func authHandler(w http.ResponseWriter, r *http.Request) {
    47  	tok, err := auth.Token(state, r)
    48  	if err != nil {
    49  		http.Error(w, "Couldn't get token", http.StatusForbidden)
    50  	}
    51  	if st := r.FormValue("state"); st != state {
    52  		http.NotFound(w, r)
    53  	}
    54  	// use the token to get an authenticated client
    55  	client := auth.NewClient(tok)
    56  	_, err = fmt.Fprintf(w, "Login Completed!")
    57  	if err != nil {
    58  		return
    59  	}
    60  	tempClientChan <- &client
    61  }
    62  
    63  // NewWidget creates a new widget for WTF
    64  func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {
    65  	redirectURI = "http://localhost:" + settings.callbackPort + "/callback"
    66  
    67  	auth = spotify.NewAuthenticator(redirectURI, spotify.ScopeUserReadCurrentlyPlaying, spotify.ScopeUserReadPlaybackState, spotify.ScopeUserModifyPlaybackState)
    68  	auth.SetAuthInfo(settings.clientID, settings.secretKey)
    69  	authURL = auth.AuthURL(state)
    70  
    71  	var client *spotify.Client
    72  	var playerState *spotify.PlayerState
    73  
    74  	widget := Widget{
    75  		TextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),
    76  
    77  		Info: Info{},
    78  
    79  		client:      client,
    80  		clientChan:  tempClientChan,
    81  		playerState: playerState,
    82  		settings:    settings,
    83  	}
    84  
    85  	http.HandleFunc("/callback", authHandler)
    86  	go func() {
    87  		err := http.ListenAndServe(":"+callbackPort, nil)
    88  		if err != nil {
    89  			return
    90  		}
    91  	}()
    92  
    93  	go func() {
    94  		// wait for auth to complete
    95  		client = <-tempClientChan
    96  
    97  		// use the client to make calls that require authorization
    98  		_, err := client.CurrentUser()
    99  		if err != nil {
   100  			panic(err)
   101  		}
   102  
   103  		playerState, err = client.PlayerState()
   104  		if err != nil {
   105  			fmt.Println(err)
   106  			os.Exit(1)
   107  		}
   108  
   109  		widget.client = client
   110  		widget.playerState = playerState
   111  		widget.Refresh()
   112  	}()
   113  
   114  	// While I wish I could find the reason this doesn't work, I can't.
   115  	//
   116  	// Normally, this should open the URL to the browser, however it opens the Explorer window in Windows.
   117  	// This mostly likely has to do with the fact that the URL includes some very special characters that no terminal likes.
   118  	// The only solution would be to include quotes in the command, which is why I do here, but it doesn't work.
   119  	//
   120  	// If inconvenient, I'll remove this option and save the URL in a file or some other method.
   121  	utils.OpenFile(`"` + authURL + `"`)
   122  
   123  	widget.settings.RefreshInterval = 5 * time.Second
   124  
   125  	widget.initializeKeyboardControls()
   126  
   127  	widget.View.SetWrap(true)
   128  	widget.View.SetWordWrap(true)
   129  
   130  	return &widget
   131  }
   132  
   133  func (w *Widget) refreshSpotifyInfos() error {
   134  	if w.client == nil || w.playerState == nil {
   135  		return errors.New("authentication failed! Please log in to Spotify by visiting the following page in your browser: " + authURL)
   136  	}
   137  	var err error
   138  	w.playerState, err = w.client.PlayerState()
   139  	if err != nil {
   140  		return errors.New("extracting player state failed! Please refresh or restart WTF")
   141  	}
   142  	w.Info.Album = fmt.Sprint(w.playerState.CurrentlyPlaying.Item.Album.Name)
   143  	artists := ""
   144  	for _, artist := range w.playerState.CurrentlyPlaying.Item.Artists {
   145  		artists += artist.Name + ", "
   146  	}
   147  	artists = artists[:len(artists)-2]
   148  	w.Info.Artists = artists
   149  	w.Info.Title = fmt.Sprint(w.playerState.CurrentlyPlaying.Item.Name)
   150  	w.Info.TrackNumber = w.playerState.CurrentlyPlaying.Item.TrackNumber
   151  	if w.playerState.CurrentlyPlaying.Playing {
   152  		w.Info.Status = "Playing"
   153  	} else {
   154  		w.Info.Status = "Paused"
   155  	}
   156  	return nil
   157  }
   158  
   159  // Refresh refreshes the current view of the widget
   160  func (w *Widget) Refresh() {
   161  	w.Redraw(w.createOutput)
   162  }
   163  
   164  func (w *Widget) createOutput() (string, string, bool) {
   165  	var output string
   166  
   167  	err := w.refreshSpotifyInfos()
   168  	if err != nil {
   169  		output = err.Error()
   170  	} else {
   171  		output += utils.CenterText(fmt.Sprintf("[green]Now %v [white]\n", w.Info.Status), w.CommonSettings().Width)
   172  		output += utils.CenterText(fmt.Sprintf("[green]Title:[white] %v\n", w.Info.Title), w.CommonSettings().Width)
   173  		output += utils.CenterText(fmt.Sprintf("[green]Artist:[white] %v\n", w.Info.Artists), w.CommonSettings().Width)
   174  		output += utils.CenterText(fmt.Sprintf("[green]Album:[white] %v\n", w.Info.Album), w.CommonSettings().Width)
   175  		if w.playerState.ShuffleState {
   176  			output += utils.CenterText("[green]Shuffle:[white] on\n", w.CommonSettings().Width)
   177  		} else {
   178  			output += utils.CenterText("[green]Shuffle:[white] off\n", w.CommonSettings().Width)
   179  		}
   180  	}
   181  	return w.CommonSettings().Title, output, true
   182  }