123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- package spotify
-
- import (
- "bytes"
- "context"
- "crypto/tls"
- "encoding/json"
- "errors"
- "fmt"
- "golang.org/x/oauth2"
- "golang.org/x/oauth2/clientcredentials"
- spotifyAuth "golang.org/x/oauth2/spotify"
- "io"
- "log"
- "net/http"
- "net/url"
- "reflect"
- "strings"
- )
-
- type SpotifyClient struct {
- clientID string
- clientSecret string
- bearerAuth string
- baseURL *url.URL
- httpClient *http.Client
- config *oauth2.Config
- context context.Context
- }
-
- type TrackInfo struct {
- Album AlbumInfo `json:"album"`
- Artists []ArtistInfo `json:"artists"`
- DiskNumber int `json:"disc_number"`
- DurationMS int `json:"duration_ms"`
- Explicit bool `json:"explicit"`
- ExternalIds ExternalIds `json:"external_ids"`
- ExternalUrls ExternalUrls `json:"external_urls"`
- Href string `json:"href"`
- ID string `json:"id"`
- IsPlayable bool `json:"is_playable"`
- Name string `json:"name"`
- Popularity int `json:"popularity"`
- PreviewUrl string `json:"preview_url"`
- TrackNumber int `json:"track_number"`
- Type string `json:"type"`
- Uri string `json:"uri"`
- }
-
- type AlbumInfo struct {
- AlbumType string `json:"album_type"`
- Artists []ArtistInfo `json:"artists"`
- ExternalUrls ExternalUrls `json:"external_urls"`
- Href string `json:"href"`
- ID string `json:"id"`
- Images []ImageInfo `json:"images"`
- Name string `json:"name"`
- Type string `json:"type"`
- Uri string `json:"uri"`
- }
-
- type ArtistInfo struct {
- ExternalUrls ExternalUrls `json:"external_urls"`
- Href string `json:"href"`
- ID string `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- Uri string `json:"uri"`
- }
-
- type ImageInfo struct {
- Height int `json:"height"`
- URL string `json:url`
- Width int `json:"width"`
- }
-
- type ExternalIds struct {
- ISRC string `json:"isrc"`
- }
- type ExternalUrls struct {
- Spotify string `json:"spotify"`
- }
-
- type TokenInfo struct {
- AccessToken string `json:"access_token"`
- TokenType string `json:"token_type"`
- ExpiresIn int `json:"expires_in"`
- }
-
- type AuthErrorResponse struct {
- Error string `json:"error"`
- ErrorDescription string `json:"error_description"`
- }
-
- type ErrorResponse struct {
- Error ErrorInfo `json:"error"`
- }
-
- type ErrorInfo struct {
- Status int `json:"status"`
- Message string `json:"message"`
- }
-
- type PlaylistInfo struct {
- Collaborative bool `json:"collaborative"`
- Description string `json:"description"`
- ExternalUrls ExternalUrls `json:"external_urls"`
- Followers Followers `json:"followers"`
- Href string `json:"href"`
- Id string `json:"id"`
- // TODO images
- Name string `json:"name"`
- // TODO owner
- Public bool `json:"public"`
- SnapshotId string `json:"snapshot_id"`
- Tracks TracksInfo `json:"tracks"`
- Type string `json:"type"`
- Uri string `json:"uri"`
- }
-
- type TracksInfo struct {
- Href string `json:"href"`
- Items []PlaylistTrack `json:"items"`
- Limit int `json:"limit"`
- Next string `json:"next"`
- Offset int `json:"offset"`
- Type string `json:"type"`
- Previous string `json:"string"`
- Total int `json:"total"`
- }
-
- type PlaylistTrack struct {
- AddedAt string `json:"added_at"`
- AddedBy string `json:"added_by"`
- IsLocal bool `json:"is_local"`
- Track TrackInfo `json":track"`
- }
-
- type Followers struct {
- Href string `json:"href"`
- Total int `json:"total"`
- }
-
- type SpotifyProfile struct {
- Birthdate string `json:"birthdate"`
- Country string `json:"country"`
- DisplayName string `json:"display_name"`
- Email string `json:"email"`
- ExternalUrls ExternalUrls `json:"external_urls"`
- Followers Followers `json:"followers"`
- Id string `json:"id"`
- Product string `json:"product"`
- Type string `json:"type"`
- Uri string `json:"uri"`
- }
-
- func NewClient(username, secret string) *SpotifyClient {
- baseURL, _ := url.Parse("https://api.spotify.com")
- httpClient := &http.Client{}
- return &SpotifyClient{
- clientID: username,
- clientSecret: secret,
- baseURL: baseURL,
- httpClient: httpClient,
- }
- }
-
- func (s *SpotifyClient) Authenticate() error {
- config := &clientcredentials.Config{
- ClientID: s.clientID,
- ClientSecret: s.clientSecret,
- TokenURL: spotifyAuth.Endpoint.TokenURL,
- }
- token, err := config.Token(context.Background())
- if err != nil {
- log.Println(err)
- return err
- }
- s.bearerAuth = token.AccessToken
- return nil
- }
-
- func (s *SpotifyClient) SetupUserAuthenticate(redirectURL string) error {
- s.config = &oauth2.Config{
- ClientID: s.clientID,
- ClientSecret: s.clientSecret,
- RedirectURL: redirectURL,
- Scopes: []string{"playlist-modify-public"},
- Endpoint: spotifyAuth.Endpoint,
- }
- tr := &http.Transport{
- TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
- }
- s.context = context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: tr})
- return nil
- }
-
- func (s *SpotifyClient) GetTrack(trackId string) (*TrackInfo, error) {
- var trackInfo TrackInfo
- url := s.urlFromBase(fmt.Sprintf("/v1/tracks/%s", trackId))
- err := s.doRequest("GET", url, &trackInfo, nil, nil)
- if err != nil {
- return nil, err
- } else {
- return &trackInfo, nil
- }
- }
-
- func (s *SpotifyClient) NewPlaylist(name, userId string) (*PlaylistInfo, error) {
- var playlistInfo PlaylistInfo
- values := &url.Values{}
- values.Add("name", name)
- url := s.urlFromBase(fmt.Sprintf("/v1/users/%s/playlists", userId))
- err := s.doRequest("POST", url, &playlistInfo, values, nil)
- if err != nil {
- return nil, err
- } else {
- return &playlistInfo, nil
- }
- }
-
- type Playlist struct {
- Uris []string `json:"uris"`
- }
-
- func (s *SpotifyClient) UpdatePlaylist(userId, playlistId string, tracks []string) error {
- uris := make([]string, 0)
- for _, track := range tracks {
- uris = append(uris, "spotify:track:"+track)
- }
- //playlist := &Playlist{uris}
- values := &url.Values{}
- values.Add("uris", strings.Join(uris, ","))
-
- result := struct {
- SnapshotID string `json:"snapshot_id"`
- }{}
-
- url := s.urlFromBase(fmt.Sprintf("/v1/users/%s/playlists/%s", userId, playlistId))
- err := s.doRequest("PUT", url, &result, values, nil)
- return err
- }
-
- func (s *SpotifyClient) GetMe() (*SpotifyProfile, error) {
- var profile SpotifyProfile
- url := s.urlFromBase(fmt.Sprintf("/v1/me"))
- err := s.doRequest("GET", url, &profile, nil, nil)
- if err != nil {
- return nil, err
- } else {
- return &profile, nil
- }
- }
-
- func (s *SpotifyClient) urlFromBase(path string) *url.URL {
- endpoint := &url.URL{Path: path}
- return s.baseURL.ResolveReference(endpoint)
- }
-
- func (s *SpotifyClient) doRequest(method string, url *url.URL, object interface{}, values *url.Values, inputBody interface{}) error {
- var bodyReader io.Reader
- var contentType string
- if inputBody != nil {
-
- //pr, pw := io.Pipe()
- //go func() {
- // err := json.NewEncoder(pw).Encode(&inputBody)
- // pw.Close()
- // if err != nil {
- // log.Printf("Spotify: Encoding body failed: %s\n", err.Error())
- // }
- //}()
-
- var buf bytes.Buffer
- err := json.NewEncoder(&buf).Encode(&inputBody)
- log.Printf("Spotify: Body is '%s'\n", buf.String())
- if err != nil {
- log.Printf("Spotify: Encoding body failed: %s\n", err.Error())
- return err
- }
-
- bodyReader = &buf
- contentType = "application/json"
- } else if values != nil {
- s := values.Encode()
- bodyReader = strings.NewReader(s)
- contentType = "application/x-www-form-urlencoded"
- }
-
- req, err := http.NewRequest(method, url.String(), bodyReader)
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", contentType)
-
- if s.bearerAuth != "" {
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.bearerAuth))
- }
-
- req.Header.Set("Accept", "application/json")
- req.Header.Set("User-Agent", "github.com/lamperi/e4bot/spotify")
-
- log.Printf("Spotify: %s %s\n", method, url.Path)
-
- resp, err := s.httpClient.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- var errorResp ErrorResponse
- err = json.NewDecoder(resp.Body).Decode(&errorResp)
- return errors.New(fmt.Sprintf("Spotify API returned: %d: %s", resp.StatusCode, errorResp.Error.Message))
- }
-
- if object != nil {
- log.Printf("Spotify: Trying to decode payload %s\n", reflect.TypeOf(object))
- err = json.NewDecoder(resp.Body).Decode(object)
- if err != nil {
- return err
- }
- }
- return nil
-
- }
-
- func (s *SpotifyClient) GetAuthUrl(state string) string {
- return s.config.AuthCodeURL(state)
- }
-
- func (s *SpotifyClient) HandleToken(state string, r *http.Request) error {
- values := r.URL.Query()
- if e := values.Get("error"); e != "" {
- return errors.New("spotify: auth failed - " + e)
- }
- code := values.Get("code")
- if code == "" {
- return errors.New("spotify: didn't get access code")
- }
- actualState := values.Get("state")
- if actualState != state {
- return errors.New("spotify: redirect state parameter doesn't match")
- }
- token, err := s.config.Exchange(s.context, code)
- if err != nil {
- return err
- }
-
- log.Println("Got token from Spotify", token)
-
- client := s.config.Client(s.context, token)
- s.httpClient = client
- return nil
- }
-
- func (s *SpotifyClient) HasUserLogin() bool {
- return s.httpClient != nil && s.config != nil && s.context != nil
- }
|